Proyectos de Subversion Moodle

Rev

| 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
 * Class abstract_data_source.
19
 *
20
 * @package    block_dash
21
 * @copyright  2019 bdecent gmbh <https://bdecent.de>
22
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 */
24
 
25
namespace block_dash\local\data_source;
26
 
27
use block_dash\local\dash_framework\query_builder\builder;
28
use block_dash\local\dash_framework\structure\field_interface;
29
use block_dash\local\dash_framework\structure\table;
30
use block_dash\local\data_grid\data\data_collection;
31
use block_dash\local\data_grid\field\attribute\identifier_attribute;
32
use block_dash\local\data_grid\data\data_collection_interface;
33
use block_dash\local\data_grid\filter\filter_collection_interface;
34
use block_dash\local\paginator;
35
use block_dash\local\data_source\form\preferences_form;
36
use block_dash\local\layout\grid_layout;
37
use block_dash\local\layout\layout_factory;
38
use block_dash\local\layout\layout_interface;
39
use coding_exception;
40
 
41
/**
42
 * Class abstract_data_source.
43
 *
44
 * @package block_dash
45
 */
46
abstract class abstract_data_source implements data_source_interface, \templatable {
47
 
48
    /**
49
     * @var \context
50
     */
51
    private $context;
52
 
53
    /**
54
     * @var data_collection_interface
55
     */
56
    private $data;
57
 
58
    /**
59
     * @var filter_collection_interface
60
     */
61
    private $filtercollection;
62
 
63
    /**
64
     * @var array
65
     */
66
    private $preferences = [];
67
 
68
    /**
69
     * @var layout_interface
70
     */
71
    private $layout;
72
 
73
    /**
74
     * @var field_interface[]
75
     */
76
    private $fields;
77
 
78
    /**
79
     * @var field_interface[]
80
     */
81
    private $sortedfields;
82
 
83
    /**
84
     * @var \block_base|null
85
     */
86
    private $blockinstance = null;
87
 
88
    /**
89
     * @var builder
90
     */
91
    private $query;
92
 
93
    /**
94
     * @var paginator
95
     */
96
    protected $paginator;
97
 
98
    /**
99
     * @var table[]
100
     */
101
    private $tables = [];
102
 
103
    /**
104
     * Constructor.
105
     *
106
     * @param \context $context
107
     */
108
    public function __construct(\context $context) {
109
        $this->context = $context;
110
    }
111
 
112
    /**
113
     * Get human readable name of data source.
114
     *
115
     * @return string
116
     */
117
    public function get_name() {
118
        return self::get_name_from_class(get_class($this));
119
    }
120
 
121
    /**
122
     * Get human readable name of data source.
123
     *
124
     * @param string $fullclassname
125
     * @param bool $help Returns the help.
126
     * @return string
127
     * @throws coding_exception
128
     */
129
    public static function get_name_from_class($fullclassname, $help=false) {
130
        $component = explode('\\', $fullclassname)[0];
131
        $class = array_reverse(explode('\\', $fullclassname))[0];
132
 
133
        $stringidentifier = "datasource:$class";
134
        $stringcomponent = $component;
135
 
136
        $stringmanager = get_string_manager();
137
        if ($stringmanager->string_exists($stringidentifier, $stringcomponent)) {
138
            $name = get_string($stringidentifier, $stringcomponent);
139
            $helpid = ['name' => $stringidentifier, 'component' => $stringcomponent];
140
        } else if ($stringmanager->string_exists($stringidentifier, 'block_dash')) {
141
            $name = get_string($stringidentifier, 'block_dash');
142
            $helpid = ['name' => $stringidentifier, 'component' => 'block_dash'];
143
        } else {
144
            $name = '[[' . $stringidentifier . ']]';
145
            $helpid = [];
146
        }
147
 
148
        if ($help && !empty($helpid)) {
149
            return ($stringmanager->string_exists($helpid['name'].'_help', $helpid['component'])) ? $helpid : [];
150
        }
151
 
152
        return ($help) ? $helpid : $name;
153
    }
154
 
155
    /**
156
     * Add table to this data source. If the table is used in a join in the main query.
157
     *
158
     * @param table $table
159
     */
160
    public function add_table(table $table): void {
161
        $this->tables[$table->get_alias()] = $table;
162
    }
163
 
164
    /**
165
     * Get tables that are in this data source's main query.
166
     *
167
     * @return array
168
     */
169
    public function get_tables(): array {
170
        return $this->tables;
171
    }
172
 
173
    /**
174
     * Get table pagination class.
175
     * @return paginator
176
     */
177
    public function get_paginator(): paginator {
178
        if ($this->get_layout()->supports_pagination()) {
179
            $perpage = (int) $this->get_preferences('perpage');
180
        }
181
        $perpage = isset($perpage) && !empty($perpage) ? $perpage : \block_dash\local\paginator::PER_PAGE_DEFAULT;
182
 
183
        if ($this->paginator == null) {
184
            $this->paginator = new paginator(function () {
185
                $count = $this->get_query()->count();
186
                if ($maxlimit = $this->get_max_limit()) {
187
                    return $maxlimit < $count ? $maxlimit : $count;
188
                }
189
                return $count;
190
            }, 0, $perpage);
191
        }
192
 
193
        return $this->paginator;
194
    }
195
 
196
    /**
197
     * Get fully built query for execution.
198
     *
199
     * @return builder
200
     */
201
    final public function get_query(): builder {
202
        if (is_null($this->query)) {
203
            $this->query = $this->get_query_template();
204
 
205
            if (count($this->get_available_fields()) == 0) {
206
                throw new \moodle_exception('Cannot build empty query in data source.');
207
            }
208
 
209
            if ($this->get_filter_collection() && $this->get_filter_collection()->has_filters()) {
210
                list ($filtersql, $filterparams) = $this->get_filter_collection()->get_sql_and_params();
211
                $this->query->where_raw($filtersql[0], $filterparams);
212
            }
213
 
214
            $fields = $this->get_available_fields();
215
 
216
            foreach ($fields as $field) {
217
                if (is_null($field->get_select())) {
218
                    continue;
219
                }
220
 
221
                $this->query->select($field->get_select(), $field->get_alias());
222
            }
223
 
224
            foreach ($this->get_available_fields() as $field) {
225
                if ($field->get_sort()) {
226
                    $this->query->orderby($field->get_select(), strtoupper($field->get_sort_direction()));
227
                }
228
            }
229
 
230
            // If there are multiple identifiers in the data source, construct a unique column.
231
            // This is to prevent warnings when multiple rows have the same value in the first column.
232
            $identifierselects = [];
233
            foreach ($this->get_available_fields() as $field) {
234
                if ($field->has_attribute(identifier_attribute::class)) {
235
                    $identifierselects[] = $field->get_select();
236
                }
237
            }
238
            global $DB;
239
            $concat = $DB->sql_concat_join("'-'", $identifierselects);
240
            if (count($identifierselects) > 1) {
241
                $this->query->select($concat, 'unique_id');
242
            }
243
 
244
            if ($this->get_layout()->supports_pagination()) {
245
                $perpage = $this->get_per_page();
246
 
247
                // Shorten per page if pagination will exceed max limit.
248
                if ($maxlimit = $this->get_max_limit()) {
249
                    if ($this->get_paginator()->get_limit_from() + $perpage > $maxlimit) {
250
                        $offset = $this->get_paginator()->get_limit_from() + $perpage - $maxlimit;
251
                        $perpage = $perpage - $offset;
252
                    }
253
                }
254
 
255
                $this->query
256
                    ->limitfrom($this->get_paginator()->get_limit_from())
257
                    ->limitnum($perpage);
258
            } else {
259
                $this->query->limitfrom(0);
260
                if ($maxlimit = $this->get_max_limit()) {
261
                    $this->query->limitnum($maxlimit);
262
                }
263
            }
264
 
265
            if ($sorting = $this->get_sorting()) {
266
                foreach ($sorting as $field => $direction) {
267
                    // Configured field is removed then remove the order.
268
                    if (is_null($this->get_field($field))) {
269
                        continue;
270
                    }
271
                    $this->query->orderby($this->get_field($field)->get_sort_select(), $direction);
272
                }
273
            }
274
        }
275
 
276
        return $this->query;
277
    }
278
 
279
    /**
280
     * Get filter collection for data grid. Build if necessary.
281
     *
282
     * @return filter_collection_interface
283
     */
284
    final public function get_filter_collection() {
285
        if (is_null($this->filtercollection)) {
286
            $this->filtercollection = $this->build_filter_collection();
287
            $this->filtercollection->init();
288
 
289
            if ($this->get_preferences('filters')) {
290
                foreach ($this->get_preferences('filters') as $filtername => $filterpreferences) {
291
                    if (is_array($filterpreferences) || is_object($filterpreferences)) {
292
                        if ($this->filtercollection->has_filter($filtername)) {
293
                            $this->filtercollection->get_filter($filtername)->set_preferences($filterpreferences);
294
                        }
295
                    }
296
                }
297
            }
298
        }
299
 
300
        return $this->filtercollection;
301
    }
302
 
303
    /**
304
     * Modify objects before data is retrieved.
305
     */
306
    public function before_data() {
307
        if ($this->get_layout()->supports_field_visibility()) {
308
            foreach ($this->get_available_fields() as $availablefield) {
309
                $availablefield->set_visibility(field_interface::VISIBILITY_HIDDEN);
310
            }
311
            if ($this->preferences && isset($this->preferences['available_fields'])) {
312
                foreach ($this->preferences['available_fields'] as $fieldname => $preferences) {
313
                    if (isset($preferences['visible'])) {
314
                        if ($field = $this->get_field($fieldname)) {
315
                            $field->set_visibility($preferences['visible']);
316
                        }
317
                    }
318
                }
319
            }
320
        }
321
 
322
        if ($this->preferences && isset($this->preferences['filters'])) {
323
            $enabledfilters = [];
324
            foreach ($this->preferences['filters'] as $filtername => $preference) {
325
                if (isset($preference['enabled']) && $preference['enabled']) {
326
                    $enabledfilters[] = $filtername;
327
                }
328
            }
329
            // No preferences set yet, remove all filters.
330
            foreach ($this->get_filter_collection()->get_filters() as $filter) {
331
                if (!in_array($filter->get_name(), $enabledfilters)) {
332
                    $this->get_filter_collection()->remove_filter($filter);
333
                }
334
            }
335
        } else {
336
            // No preferences set yet, remove all filters.
337
            foreach ($this->get_filter_collection()->get_filters() as $filter) {
338
                $this->get_filter_collection()->remove_filter($filter);
339
            }
340
        }
341
 
342
        $this->get_layout()->before_data();
343
    }
344
 
345
    /**
346
     * Get data collection.
347
     *
348
     * @return data_collection_interface
349
     * @throws \moodle_exception
350
     */
351
    final public function get_data() {
352
        if (is_null($this->data)) {
353
            // If the block has no preferences do not query any data.
354
            if (empty($this->get_all_preferences())) {
355
                return block_dash_get_data_collection();
356
            }
357
 
358
            $this->before_data();
359
 
360
            if (!$strategy = $this->get_layout()->get_data_strategy()) {
361
                throw new coding_exception('Not fully configured.');
362
            }
363
 
364
            if ($this->is_widget()) {
365
                $this->data = $this->get_widget_data();
366
            } else {
367
                $records = $this->get_query()->query();
368
                $this->data = $strategy->convert_records_to_data_collection($records, $this->get_sorted_fields());
369
                if ($modifieddata = $this->after_data($this->data)) {
370
                    $this->data = $modifieddata;
371
                }
372
            }
373
        }
374
        return $this->data;
375
    }
376
 
377
    /**
378
     * Modify objects after data is retrieved.
379
     *
380
     * @param data_collection_interface $datacollection
381
     */
382
    public function after_data(data_collection_interface $datacollection) {
383
        return $this->get_layout()->after_data($datacollection);
384
    }
385
 
386
    /**
387
     * Explicitly set layout.
388
     *
389
     * @param layout_interface $layout
390
     */
391
    public function set_layout(layout_interface $layout) {
392
        $this->layout = $layout;
393
    }
394
 
395
    /**
396
     * Get layout.
397
     *
398
     * @return layout_interface
399
     */
400
    public function get_layout() {
401
        if (is_null($this->layout)) {
402
            if ($layout = $this->get_preferences('layout')) {
403
                $this->layout = layout_factory::build_layout($layout, $this);
404
            }
405
 
406
            // If still null default to grid layout.
407
            if (is_null($this->layout)) {
408
                $this->layout = new grid_layout($this);
409
            }
410
        }
411
 
412
        return $this->layout;
413
    }
414
 
415
    /**
416
     * Get context.
417
     *
418
     * @return \context
419
     */
420
    public function get_context() {
421
        return $this->context;
422
    }
423
 
424
    /**
425
     * Get template variables.
426
     *
427
     * @param \renderer_base $output
428
     * @return array|\renderer_base|\stdClass|string
429
     * @throws coding_exception
430
     */
431
    final public function export_for_template(\renderer_base $output) {
432
        $data = $this->get_layout()->export_for_template($output);
433
        $data['datasource'] = $this;
434
        return $data;
435
    }
436
 
437
    /**
438
     * Add form fields to the block edit form. IMPORTANT: Prefix field names with config_ otherwise the values will
439
     * not be saved.
440
     *
441
     * @param \moodleform $form
442
     * @param \MoodleQuickForm $mform
443
     * @throws coding_exception
444
     */
445
    public function build_preferences_form(\moodleform $form, \MoodleQuickForm $mform) {
446
        if ($form->get_tab() == preferences_form::TAB_GENERAL) {
447
            $mform->addElement('static', 'data_source_name', get_string('datasource', 'block_dash'), $this->get_name());
448
 
449
            $mform->addElement('select', 'config_preferences[layout]', get_string('layout', 'block_dash'),
450
                layout_factory::get_layout_form_options());
451
            $mform->setType('config_preferences[layout]', PARAM_TEXT);
452
        }
453
 
454
        if ($layout = $this->get_layout()) {
455
            $layout->build_preferences_form($form, $mform);
456
        }
457
 
458
        if ($form->get_tab() == preferences_form::TAB_FIELDS) {
459
            $mform->addElement('html', '<hr>');
460
 
461
            $sortablefields = [];
462
            foreach ($this->get_available_fields() as $field) {
463
                if ($field->get_option('supports_sorting') !== false) {
464
                    $sortablefields[$field->get_alias()] = $field->get_table()->get_title() . ': ' . $field->get_title();
465
                }
466
            }
467
 
468
            $mform->addElement('select', 'config_preferences[default_sort]', get_string('defaultsortfield', 'block_dash'),
469
                $sortablefields);
470
            $mform->setType('config_preferences[default_sort]', PARAM_TEXT);
471
            $mform->addHelpButton('config_preferences[default_sort]', 'defaultsortfield', 'block_dash');
472
 
473
            $mform->addElement('select', 'config_preferences[default_sort_direction]',
474
                get_string('defaultsortdirection', 'block_dash'), [ 'asc' => 'ASC', 'desc' => 'DESC']
475
            );
476
            $mform->setType('config_preferences[default_sort_direction]', PARAM_TEXT);
477
 
478
            $mform->addElement('text', 'config_preferences[maxlimit]', get_string('maxlimit', 'block_dash'));
479
            $mform->setType('config_preferences[maxlimit]', PARAM_INT);
480
            $mform->addHelpButton('config_preferences[maxlimit]', 'maxlimit', 'block_dash');
481
 
482
            $mform->addElement('text', 'config_preferences[perpage]', get_string('perpage', 'block_dash'));
483
            $mform->setType('config_preferences[perpage]', PARAM_INT);
484
            $mform->addHelpButton('config_preferences[perpage]', 'perpage', 'block_dash');
485
        }
486
    }
487
 
488
    // Region Preferences.
489
 
490
    /**
491
     * Get a specific preference.
492
     *
493
     * @param string $name
494
     * @return mixed|array
495
     */
496
    final public function get_preferences($name) {
497
        if ($this->preferences && isset($this->preferences[$name])) {
498
            return $this->preferences[$name];
499
        }
500
        return null;
501
    }
502
 
503
    /**
504
     * Get all preferences associated with the data source.
505
     *
506
     * @return array
507
     */
508
    final public function get_all_preferences() {
509
        return $this->preferences;
510
    }
511
 
512
    /**
513
     * Set preferences on this data source.
514
     *
515
     * @param array $preferences
516
     */
517
    final public function set_preferences(array $preferences) {
518
        $this->preferences = $preferences;
519
    }
520
 
521
    // Endregion.
522
 
523
    /**
524
     * Get count query template.
525
     *
526
     * @return string
527
     */
528
    public function get_count_query_template() {
529
        return $this->get_query_template();
530
    }
531
 
532
    /**
533
     * Get group by fields.
534
     *
535
     * @return string
536
     */
537
    public function get_groupby() {
538
        return false;
539
    }
540
 
541
    /**
542
     * Get available field definitions.
543
     *
544
     * @return field_interface[]
545
     */
546
    final public function get_available_fields() {
547
        if (is_null($this->fields)) {
548
            // Get all available field definitions based on tables.
549
            $this->fields = [];
550
            foreach ($this->get_tables() as $table) {
551
                foreach ($table->get_fields() as $field) {
552
                    $this->fields[$field->get_alias()] = $field;
553
                }
554
            }
555
        }
556
 
557
        return $this->fields;
558
    }
559
 
560
    /**
561
     * Check if report has a certain field
562
     *
563
     * @param string $alias Field alias.
564
     * @return bool
565
     */
566
    public function has_field(string $alias): bool {
567
        return isset($this->get_available_fields()[$alias]);
568
    }
569
 
570
    /**
571
     * Get field by name. Returns null if not found.
572
     *
573
     * @param string $alias Field alias.
574
     * @return ?field_interface
575
     */
576
    public function get_field(string $alias): ?field_interface {
577
        // Fields are keyed by name.
578
        if ($this->has_field($alias)) {
579
            return $this->get_available_fields()[$alias];
580
        }
581
 
582
        return null;
583
    }
584
 
585
    /**
586
     * Get sorted field definitions based on preferences.
587
     *
588
     * @return field_interface[]
589
     */
590
    public function get_sorted_fields() {
591
        if (is_null($this->sortedfields)) {
592
            $fields = $this->get_available_fields();;
593
 
594
            if ($this->get_layout()->supports_field_visibility()) {
595
 
596
                $sortedfields = [];
597
 
598
                // First add the identifiers, in order, so they always come first in the query.
599
                foreach ($fields as $key => $field) {
600
                    if ($field->has_attribute(identifier_attribute::class)) {
601
                        $sortedfields[] = $field;
602
                        unset($fields[$key]);
603
                    }
604
                }
605
 
606
                if ($availablefields = $this->get_preferences('available_fields')) {
607
                    foreach ($availablefields as $fieldalias => $availablefield) {
608
                        foreach ($fields as $key => $field) {
609
                            if ($field->get_alias() == $fieldalias) {
610
                                $sortedfields[] = $field;
611
                                unset($fields[$key]);
612
                                break;
613
                            }
614
                        }
615
                    }
616
 
617
                    foreach ($fields as $field) {
618
                        $sortedfields[] = $field;
619
                    }
620
 
621
                    $fields = $sortedfields;
622
                }
623
 
624
            }
625
 
626
            $this->sortedfields = array_values($fields);
627
        }
628
 
629
        return $this->sortedfields;
630
    }
631
 
632
    /**
633
     * Get sorting.
634
     *
635
     * @throws coding_exception
636
     */
637
    public function get_sorting() {
638
        global $USER;
639
 
640
        if ($this->get_layout()->supports_sorting() && $this->get_block_instance()) {
641
            $cache = \cache::make_from_params(\cache_store::MODE_SESSION, 'block_dash', 'sort');
642
 
643
            if ($cache->has($USER->id . '_' . $this->get_block_instance()->instance->id)) {
644
                return $cache->get($USER->id . '_' . $this->get_block_instance()->instance->id);
645
            }
646
        }
647
 
648
        if ($defaultsort = $this->get_preferences('default_sort')) {
649
            return [$defaultsort => $this->get_preferences('default_sort_direction') ?? 'asc'];
650
        }
651
 
652
        return [];
653
    }
654
 
655
    /**
656
     * Get maximum number of records to query.
657
     *
658
     * @return ?int
659
     */
660
    public function get_max_limit() {
661
        return $this->get_preferences('maxlimit');
662
    }
663
 
664
    /**
665
     * Get per page number for pagination.
666
     *
667
     * @return ?int
668
     */
669
    public function get_per_page() {
670
        if ($perpage = $this->get_preferences('perpage')) {
671
            return $perpage;
672
        }
673
        return $this->get_paginator()->get_per_page();
674
    }
675
 
676
    /**
677
     * Set block instance.
678
     *
679
     * @param \block_base $blockinstance
680
     */
681
    public function set_block_instance(\block_base $blockinstance) {
682
        $this->blockinstance = $blockinstance;
683
    }
684
 
685
    /**
686
     * Get block instance.
687
     *
688
     * @return null|\block_base
689
     */
690
    public function get_block_instance() {
691
        return $this->blockinstance;
692
    }
693
 
694
    /**
695
     * Update the block fetched data before render.
696
     *
697
     * @param array $data
698
     * @return void
699
     */
700
    public function update_data_before_render(&$data) {
701
        return null;
702
    }
703
 
704
    /**
705
     * Set the data source supports debug.
706
     *
707
     * @return bool;
708
     */
709
    public function supports_debug() {
710
        return true;
711
    }
712
 
713
    /**
714
     * Type of the data source.
715
     *
716
     * @return boolean
717
     */
718
    public function is_widget() {
719
        return false;
720
    }
721
 
722
}