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
declare(strict_types=1);
18
 
19
namespace core_reportbuilder\local\filters;
20
 
1441 ariadna 21
use core\{clock, di};
22
use core\lang_string;
23
use core_reportbuilder\local\helpers\database;
1 efrain 24
use MoodleQuickForm;
25
 
26
/**
27
 * Date report filter
28
 *
29
 * This filter accepts a unix timestamp to perform date filtering on
30
 *
31
 * @package     core_reportbuilder
32
 * @copyright   2021 Paul Holden <paulh@moodle.com>
33
 * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
34
 */
35
class date extends base {
36
 
37
    /** @var int Any value */
38
    public const DATE_ANY = 0;
39
 
40
    /** @var int Non-empty (positive) value */
41
    public const DATE_NOT_EMPTY = 1;
42
 
43
    /** @var int Empty (zero) value */
44
    public const DATE_EMPTY = 2;
45
 
46
    /** @var int Date within defined range */
47
    public const DATE_RANGE = 3;
48
 
49
    /** @var int Date in the last [X relative date unit(s)] */
50
    public const DATE_LAST = 4;
51
 
52
    /** @var int Date in the previous [X relative date unit(s)] Kept for backwards compatibility */
53
    public const DATE_PREVIOUS = self::DATE_LAST;
54
 
55
    /** @var int Date in current [relative date unit] */
56
    public const DATE_CURRENT = 5;
57
 
58
    /** @var int Date in the next [X relative date unit(s)] */
59
    public const DATE_NEXT = 6;
60
 
61
    /** @var int Date in the past */
62
    public const DATE_PAST = 7;
63
 
64
    /** @var int Date in the future */
65
    public const DATE_FUTURE = 8;
66
 
67
    /** @var int Date before [X relative date unit(s)] */
68
    public const DATE_BEFORE = 9;
69
 
70
    /** @var int Date after [X relative date unit(s)] */
71
    public const DATE_AFTER = 10;
72
 
1441 ariadna 73
    /** @var int Relative date unit for a minute */
74
    public const DATE_UNIT_MINUTE = 5;
75
 
1 efrain 76
    /** @var int Relative date unit for an hour */
77
    public const DATE_UNIT_HOUR = 0;
78
 
79
    /** @var int Relative date unit for a day */
80
    public const DATE_UNIT_DAY = 1;
81
 
82
    /** @var int Relative date unit for a week */
83
    public const DATE_UNIT_WEEK = 2;
84
 
85
    /** @var int Relative date unit for a month */
86
    public const DATE_UNIT_MONTH = 3;
87
 
88
    /** @var int Relative date unit for a month */
89
    public const DATE_UNIT_YEAR = 4;
90
 
91
    /**
92
     * Return an array of operators available for this filter
93
     *
94
     * @return lang_string[]
95
     */
96
    private function get_operators(): array {
97
        $operators = [
98
            self::DATE_ANY => new lang_string('filterisanyvalue', 'core_reportbuilder'),
99
            self::DATE_NOT_EMPTY => new lang_string('filterisnotempty', 'core_reportbuilder'),
100
            self::DATE_EMPTY => new lang_string('filterisempty', 'core_reportbuilder'),
101
            self::DATE_RANGE => new lang_string('filterrange', 'core_reportbuilder'),
102
            self::DATE_BEFORE => new lang_string('filterdatebefore', 'core_reportbuilder'),
103
            self::DATE_AFTER => new lang_string('filterdateafter', 'core_reportbuilder'),
104
            self::DATE_LAST => new lang_string('filterdatelast', 'core_reportbuilder'),
105
            self::DATE_CURRENT => new lang_string('filterdatecurrent', 'core_reportbuilder'),
106
            self::DATE_NEXT => new lang_string('filterdatenext', 'core_reportbuilder'),
107
            self::DATE_PAST => new lang_string('filterdatepast', 'core_reportbuilder'),
108
            self::DATE_FUTURE => new lang_string('filterdatefuture', 'core_reportbuilder'),
109
        ];
110
 
111
        return $this->filter->restrict_limited_operators($operators);
112
    }
113
 
114
    /**
115
     * Setup form
116
     *
1441 ariadna 117
     * Note that we cannot support float inputs in this filter currently, because decimals are not supported when calculating
118
     * relative timeframes according to {@link https://www.php.net/manual/en/datetime.formats.php}
119
     *
1 efrain 120
     * @param MoodleQuickForm $mform
121
     */
122
    public function setup_form(MoodleQuickForm $mform): void {
123
        // Operator selector.
124
        $operatorlabel = get_string('filterfieldoperator', 'core_reportbuilder', $this->get_header());
125
        $typesnounit = [self::DATE_ANY, self::DATE_NOT_EMPTY, self::DATE_EMPTY, self::DATE_RANGE,
126
            self::DATE_PAST, self::DATE_FUTURE];
127
 
128
        $elements[] = $mform->createElement('select', "{$this->name}_operator", $operatorlabel, $this->get_operators());
129
        $mform->setType("{$this->name}_operator", PARAM_INT);
130
        $mform->setDefault("{$this->name}_operator", self::DATE_ANY);
131
 
132
        // Value selector for last and next operators.
133
        $valuelabel = get_string('filterfieldvalue', 'core_reportbuilder', $this->get_header());
134
 
135
        $elements[] = $mform->createElement('text', "{$this->name}_value", $valuelabel, ['size' => 3]);
136
        $mform->setType("{$this->name}_value", PARAM_INT);
137
        $mform->setDefault("{$this->name}_value", 1);
138
        $mform->hideIf("{$this->name}_value", "{$this->name}_operator", 'in', array_merge($typesnounit, [self::DATE_CURRENT]));
139
 
140
        // Unit selector for last and next operators.
141
        $unitlabel = get_string('filterfieldunit', 'core_reportbuilder', $this->get_header());
142
        $units = [
1441 ariadna 143
            self::DATE_UNIT_MINUTE => get_string('filterdateminutes', 'core_reportbuilder'),
1 efrain 144
            self::DATE_UNIT_HOUR => get_string('filterdatehours', 'core_reportbuilder'),
145
            self::DATE_UNIT_DAY => get_string('filterdatedays', 'core_reportbuilder'),
146
            self::DATE_UNIT_WEEK => get_string('filterdateweeks', 'core_reportbuilder'),
147
            self::DATE_UNIT_MONTH => get_string('filterdatemonths', 'core_reportbuilder'),
148
            self::DATE_UNIT_YEAR => get_string('filterdateyears', 'core_reportbuilder'),
149
        ];
150
 
151
        $elements[] = $mform->createElement('select', "{$this->name}_unit", $unitlabel, $units);
152
        $mform->setType("{$this->name}_unit", PARAM_INT);
153
        $mform->setDefault("{$this->name}_unit", self::DATE_UNIT_DAY);
154
        $mform->hideIf("{$this->name}_unit", "{$this->name}_operator", 'in', $typesnounit);
155
 
156
        // Add operator/value/unit group.
157
        $mform->addGroup($elements, "{$this->name}_group", $this->get_header(), '', false)
158
            ->setHiddenLabel(true);
159
 
160
        // Date selectors for range operator.
1441 ariadna 161
        $mform->addElement('date_selector', "{$this->name}_from",
162
            get_string('filterfieldfrom', 'core_reportbuilder', $this->get_header()), ['optional' => true]);
1 efrain 163
        $mform->setType("{$this->name}_from", PARAM_INT);
164
        $mform->setDefault("{$this->name}_from", 0);
165
        $mform->hideIf("{$this->name}_from", "{$this->name}_operator", 'neq', self::DATE_RANGE);
166
 
1441 ariadna 167
        $mform->addElement('date_selector', "{$this->name}_to",
168
            get_string('filterfieldto', 'core_reportbuilder', $this->get_header()), ['optional' => true]);
1 efrain 169
        $mform->setType("{$this->name}_to", PARAM_INT);
170
        $mform->setDefault("{$this->name}_to", 0);
171
        $mform->hideIf("{$this->name}_to", "{$this->name}_operator", 'neq', self::DATE_RANGE);
172
    }
173
 
174
    /**
175
     * Return filter SQL
176
     *
177
     * @param array $values
178
     * @return array
179
     */
180
    public function get_sql_filter(array $values): array {
181
        $fieldsql = $this->filter->get_field_sql();
182
        $params = $this->filter->get_field_params();
183
 
184
        $operator = (int) ($values["{$this->name}_operator"] ?? self::DATE_ANY);
185
        $dateunitvalue = (int) ($values["{$this->name}_value"] ?? 1);
186
        $dateunit = (int) ($values["{$this->name}_unit"] ?? self::DATE_UNIT_DAY);
187
 
188
        switch ($operator) {
189
            case self::DATE_NOT_EMPTY:
190
                $sql = "COALESCE({$fieldsql}, 0) <> 0";
191
                break;
192
            case self::DATE_EMPTY:
193
                $sql = "COALESCE({$fieldsql}, 0) = 0";
194
                break;
195
            case self::DATE_RANGE:
196
                $sql = '';
197
 
198
                $datefrom = (int)($values["{$this->name}_from"] ?? 0);
199
                $dateto = (int)($values["{$this->name}_to"] ?? 0);
200
 
201
                [$paramdatefrom, $paramdateto] = database::generate_param_names(2);
202
 
203
                if ($datefrom > 0 && $dateto > 0) {
204
                    $sql = "{$fieldsql} BETWEEN :{$paramdatefrom} AND :{$paramdateto}";
205
                    $params[$paramdatefrom] = $datefrom;
206
                    $params[$paramdateto] = $dateto;
207
                } else if ($datefrom > 0) {
208
                    $sql = "{$fieldsql} >= :{$paramdatefrom}";
209
                    $params[$paramdatefrom] = $datefrom;
210
                } else if ($dateto > 0) {
211
                    $sql = "{$fieldsql} < :{$paramdateto}";
212
                    $params[$paramdateto] = $dateto;
213
                }
214
 
215
                break;
216
            case self::DATE_BEFORE:
217
                $param = database::generate_param_name();
218
 
219
                // We can use the start date of the "Last" operator as the end date here.
220
                $sql = "{$fieldsql} < :{$param}";
221
                $params[$param] = self::get_relative_timeframe(self::DATE_LAST, $dateunitvalue, $dateunit)[0];
222
                break;
223
            case self::DATE_AFTER:
224
                $param = database::generate_param_name();
225
 
226
                // We can use the end date of the "Next" operator as the start date here.
227
                $sql = "{$fieldsql} > :{$param}";
228
                $params[$param] = self::get_relative_timeframe(self::DATE_NEXT, $dateunitvalue, $dateunit)[1];
229
                break;
230
            // Relative helper method can handle these three cases.
231
            case self::DATE_LAST:
232
            case self::DATE_CURRENT:
233
            case self::DATE_NEXT:
234
 
235
                // Last and next operators require a unit value greater than zero.
236
                if ($operator !== self::DATE_CURRENT && $dateunitvalue === 0) {
237
                    return ['', []];
238
                }
239
 
240
                // Generate parameters and SQL clause for the relative date comparison.
241
                [$paramdatefrom, $paramdateto] = database::generate_param_names(2);
242
                $sql = "{$fieldsql} BETWEEN :{$paramdatefrom} AND :{$paramdateto}";
243
 
244
                [
245
                    $params[$paramdatefrom],
246
                    $params[$paramdateto],
247
                ] = self::get_relative_timeframe($operator, $dateunitvalue, $dateunit);
248
 
249
                break;
250
            case self::DATE_PAST:
251
                $param = database::generate_param_name();
252
                $sql = "{$fieldsql} < :{$param}";
1441 ariadna 253
                $params[$param] = di::get(clock::class)->time();
1 efrain 254
                break;
255
            case self::DATE_FUTURE:
256
                $param = database::generate_param_name();
257
                $sql = "{$fieldsql} > :{$param}";
1441 ariadna 258
                $params[$param] = di::get(clock::class)->time();
1 efrain 259
                break;
260
            default:
261
                // Invalid or inactive filter.
262
                return ['', []];
263
        }
264
 
265
        return [$sql, $params];
266
    }
267
 
268
    /**
269
     * Return start and end time of given relative date period
270
     *
271
     * @param int $operator One of the ::DATE_LAST/CURRENT/NEXT constants
272
     * @param int $dateunitvalue Unit multiplier of the date unit
273
     * @param int $dateunit One of the ::DATE_UNIT_* constants
274
     * @return int[] Timestamps representing the start/end of timeframe
275
     */
276
    private static function get_relative_timeframe(int $operator, int $dateunitvalue, int $dateunit): array {
277
        // Initialise start/end time to now.
1441 ariadna 278
        $datestart = $dateend = di::get(clock::class)->now();
1 efrain 279
 
280
        switch ($dateunit) {
1441 ariadna 281
            case self::DATE_UNIT_MINUTE:
282
                if ($operator === self::DATE_CURRENT) {
283
                    $hour = (int) $datestart->format('G');
284
                    $minute = (int) $datestart->format('i');
285
                    $datestart = $datestart->setTime($hour, $minute);
286
                    $dateend = $dateend->setTime($hour, $minute, 59);
287
                } else if ($operator === self::DATE_LAST) {
288
                    $datestart = $datestart->modify("-{$dateunitvalue} minute");
289
                } else if ($operator === self::DATE_NEXT) {
290
                    $dateend = $dateend->modify("+{$dateunitvalue} minute");
291
                }
292
                break;
1 efrain 293
            case self::DATE_UNIT_HOUR:
294
                if ($operator === self::DATE_CURRENT) {
295
                    $hour = (int) $datestart->format('G');
296
                    $datestart = $datestart->setTime($hour, 0);
297
                    $dateend = $dateend->setTime($hour, 59, 59);
298
                } else if ($operator === self::DATE_LAST) {
299
                    $datestart = $datestart->modify("-{$dateunitvalue} hour");
300
                } else if ($operator === self::DATE_NEXT) {
301
                    $dateend = $dateend->modify("+{$dateunitvalue} hour");
302
                }
303
                break;
304
            case self::DATE_UNIT_DAY:
305
                if ($operator === self::DATE_CURRENT) {
306
                    $datestart = $datestart->setTime(0, 0);
307
                    $dateend = $dateend->setTime(23, 59, 59);
308
                } else if ($operator === self::DATE_LAST) {
309
                    $datestart = $datestart->modify("-{$dateunitvalue} day");
310
                } else if ($operator === self::DATE_NEXT) {
311
                    $dateend = $dateend->modify("+{$dateunitvalue} day");
312
                }
313
 
314
                break;
315
            case self::DATE_UNIT_WEEK:
316
                if ($operator === self::DATE_CURRENT) {
317
                    // The first day of the week is determined by site calendar configuration/preferences.
318
                    $startweekday = \core_calendar\type_factory::get_calendar_instance()->get_starting_weekday();
319
                    $weekdays = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
320
 
321
                    // If calculated start of week is after today (today is Tues/start of week is Weds), move back a week.
322
                    $datestartnow = $datestart->getTimestamp();
323
                    $datestart = $datestart->modify($weekdays[$startweekday] . ' this week')->setTime(0, 0);
324
                    if ($datestart->getTimestamp() > $datestartnow) {
325
                        $datestart = $datestart->modify('-1 week');
326
                    }
327
 
328
                    $dateend = $datestart->modify('+6 day')->setTime(23, 59, 59);
329
                } else if ($operator === self::DATE_LAST) {
330
                    $datestart = $datestart->modify("-{$dateunitvalue} week");
331
                } else if ($operator === self::DATE_NEXT) {
332
                    $dateend = $dateend->modify("+{$dateunitvalue} week");
333
                }
334
 
335
                break;
336
            case self::DATE_UNIT_MONTH:
337
                if ($operator === self::DATE_CURRENT) {
338
                    $datestart = $datestart->modify('first day of this month')->setTime(0, 0);
339
                    $dateend = $dateend->modify('last day of this month')->setTime(23, 59, 59);
340
                } else if ($operator === self::DATE_LAST) {
341
                    $datestart = $datestart->modify("-{$dateunitvalue} month");
342
                } else if ($operator === self::DATE_NEXT) {
343
                    $dateend = $dateend->modify("+{$dateunitvalue} month");
344
                }
345
 
346
                break;
347
            case self::DATE_UNIT_YEAR:
348
                if ($operator === self::DATE_CURRENT) {
349
                    $datestart = $datestart->modify('first day of january this year')->setTime(0, 0);
350
                    $dateend = $dateend->modify('last day of december this year')->setTime(23, 59, 59);
351
                } else if ($operator === self::DATE_LAST) {
352
                    $datestart = $datestart->modify("-{$dateunitvalue} year");
353
                } else if ($operator === self::DATE_NEXT) {
354
                    $dateend = $dateend->modify("+{$dateunitvalue} year");
355
                }
356
 
357
                break;
358
        }
359
 
360
        return [
361
            $datestart->getTimestamp(),
362
            $dateend->getTimestamp(),
363
        ];
364
    }
365
 
366
    /**
367
     * Return sample filter values
368
     *
369
     * @return array
370
     */
371
    public function get_sample_values(): array {
372
        return [
373
            "{$this->name}_operator" => self::DATE_CURRENT,
374
            "{$this->name}_unit" => self::DATE_UNIT_WEEK,
375
        ];
376
    }
377
}