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\helpers;
20
 
21
use context_user;
1441 ariadna 22
use core\{clock, di};
1 efrain 23
use core_user;
24
use invalid_parameter_exception;
25
use stdClass;
26
use stored_file;
27
use table_dataformat_export_format;
28
use core\message\message;
29
use core\plugininfo\dataformat;
30
use core_reportbuilder\local\models\audience as audience_model;
31
use core_reportbuilder\local\models\schedule as model;
32
use core_reportbuilder\table\custom_report_table_view;
33
 
34
/**
35
 * Helper class for report schedule related methods
36
 *
37
 * @package     core_reportbuilder
38
 * @copyright   2021 Paul Holden <paulh@moodle.com>
39
 * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
40
 */
41
class schedule {
42
 
43
    /**
44
     * Create report schedule, calculate when it should be next sent
45
     *
46
     * @param stdClass $data
1441 ariadna 47
     * @param int|null $timenow Deprecated since Moodle 4.5 - please use {@see clock} dependency injection
1 efrain 48
     * @return model
49
     */
50
    public static function create_schedule(stdClass $data, ?int $timenow = null): model {
1441 ariadna 51
        if ($timenow !== null) {
52
            debugging('Passing $timenow is deprecated, please use \core\clock dependency injection', DEBUG_DEVELOPER);
53
        }
54
 
1 efrain 55
        $data->name = trim($data->name);
56
 
57
        $schedule = (new model(0, $data));
1441 ariadna 58
        $schedule->set('timenextsend', self::calculate_next_send_time($schedule));
1 efrain 59
 
60
        return $schedule->create();
61
    }
62
 
63
    /**
64
     * Update report schedule
65
     *
66
     * @param stdClass $data
67
     * @return model
68
     * @throws invalid_parameter_exception
69
     */
70
    public static function update_schedule(stdClass $data): model {
71
        $schedule = model::get_record(['id' => $data->id, 'reportid' => $data->reportid]);
72
        if ($schedule === false) {
73
            throw new invalid_parameter_exception('Invalid schedule');
74
        }
75
 
76
        // Normalize model properties.
77
        $data = array_intersect_key((array) $data, model::properties_definition());
78
        if (array_key_exists('name', $data)) {
79
            $data['name'] = trim($data['name']);
80
        }
81
 
82
        $schedule->set_many($data);
83
        $schedule->set('timenextsend', self::calculate_next_send_time($schedule))
84
            ->update();
85
 
86
        return $schedule;
87
    }
88
 
89
    /**
90
     * Toggle report schedule enabled
91
     *
92
     * @param int $reportid
93
     * @param int $scheduleid
94
     * @param bool $enabled
95
     * @return bool
96
     * @throws invalid_parameter_exception
97
     */
98
    public static function toggle_schedule(int $reportid, int $scheduleid, bool $enabled): bool {
99
        $schedule = model::get_record(['id' => $scheduleid, 'reportid' => $reportid]);
100
        if ($schedule === false) {
101
            throw new invalid_parameter_exception('Invalid schedule');
102
        }
103
 
104
        return $schedule->set('enabled', $enabled)->update();
105
    }
106
 
107
    /**
108
     * Return array of users who match the audience records added to the given schedule
109
     *
110
     * @param model $schedule
111
     * @return stdClass[]
112
     */
113
    public static function get_schedule_report_users(model $schedule): array {
114
        global $DB;
115
 
116
        $audienceids = (array) json_decode($schedule->get('audiences'));
117
 
118
        // Retrieve all selected audience records for the schedule.
119
        [$audienceselect, $audienceparams] = $DB->get_in_or_equal($audienceids, SQL_PARAMS_NAMED, 'aid', true, null);
120
        $audiences = audience_model::get_records_select("id {$audienceselect}", $audienceparams);
121
 
122
        // Now convert audiences to SQL for user retrieval.
123
        [$wheres, $params] = audience::user_audience_sql($audiences);
124
        if (count($wheres) === 0) {
125
            return [];
126
        }
127
 
128
        [$userorder] = users_order_by_sql('u');
129
 
130
        $sql = 'SELECT u.*
131
                  FROM {user} u
132
                 WHERE (' . implode(' OR ', $wheres) . ')
133
                   AND u.deleted = 0
134
              ORDER BY ' . $userorder;
135
 
136
        return $DB->get_records_sql($sql, $params);
137
    }
138
 
139
    /**
140
     * Return count of schedule report rows
141
     *
142
     * @param model $schedule
143
     * @return int
1441 ariadna 144
     *
145
     * @deprecated since Moodle 5.0 - please do not use this function any more, {@see report::get_report_row_count}
1 efrain 146
     */
1441 ariadna 147
    #[\core\attribute\deprecated('report::get_report_row_count', since: '5.0', mdl: 'MDL-74488')]
1 efrain 148
    public static function get_schedule_report_count(model $schedule): int {
1441 ariadna 149
        \core\deprecation::emit_deprecation([self::class, __FUNCTION__]);
1 efrain 150
 
1441 ariadna 151
        return report::get_report_row_count($schedule->get('reportid'));
1 efrain 152
    }
153
 
154
    /**
155
     * Generate stored file instance for given schedule, in user draft
156
     *
157
     * @param model $schedule
158
     * @return stored_file
159
     */
160
    public static function get_schedule_report_file(model $schedule): stored_file {
161
        global $CFG, $USER;
162
 
163
        require_once("{$CFG->libdir}/filelib.php");
164
 
165
        $table = custom_report_table_view::create($schedule->get('reportid'));
166
        $table->setup();
167
        $table->query_db(0, false);
168
 
169
        // Set up table as if it were being downloaded, retrieve appropriate export class (ensure output buffer is
170
        // cleaned in order to instantiate export class without exception).
171
        ob_start();
172
        $table->download = $schedule->get('format');
173
        $exportclass = new table_dataformat_export_format($table, $table->download);
174
        ob_end_clean();
175
 
176
        // Create our schedule report stored file temporarily in user draft.
177
        $filerecord = [
178
            'contextid' => context_user::instance($USER->id)->id,
179
            'component' => 'user',
180
            'filearea' => 'draft',
181
            'itemid' => file_get_unused_draft_itemid(),
182
            'filepath' => '/',
183
            'filename' => clean_filename($schedule->get_formatted_name()),
184
        ];
185
 
186
        $storedfile = \core\dataformat::write_data_to_filearea(
187
            $filerecord,
188
            $table->download,
189
            $exportclass->format_data($table->headers),
190
            $table->rawdata,
191
            static function(stdClass $record, bool $supportshtml) use ($table, $exportclass): array {
192
                $record = $table->format_row($record);
193
                if (!$supportshtml) {
194
                    $record = $exportclass->format_data($record);
195
                }
196
                return $record;
197
            }
198
        );
199
 
200
        $table->close_recordset();
201
 
202
        return $storedfile;
203
    }
204
 
205
    /**
206
     * Check whether given schedule needs to be sent
207
     *
208
     * @param model $schedule
209
     * @return bool
210
     */
211
    public static function should_send_schedule(model $schedule): bool {
212
        if (!$schedule->get('enabled')) {
213
            return false;
214
        }
215
 
1441 ariadna 216
        $timenow = di::get(clock::class)->time();
1 efrain 217
 
218
        // Ensure we've reached the initial scheduled start time.
219
        $timescheduled = $schedule->get('timescheduled');
220
        if ($timescheduled > $timenow) {
221
            return false;
222
        }
223
 
224
        // If there's no recurrence, check whether it's been sent since initial scheduled start time. This ensures that even if
225
        // the schedule was manually sent beforehand, it'll still be automatically sent once the start time is first reached.
226
        if ($schedule->get('recurrence') === model::RECURRENCE_NONE) {
227
            return $schedule->get('timelastsent') < $timescheduled;
228
        }
229
 
230
        return $schedule->get('timenextsend') <= $timenow;
231
    }
232
 
233
    /**
234
     * Calculate the next time a schedule should be sent, based on it's recurrence and when it was initially scheduled. Ensures
235
     * returned value is after the current date
236
     *
237
     * @param model $schedule
1441 ariadna 238
     * @param int|null $timenow Deprecated since Moodle 4.5 - please use {@see clock} dependency injection
1 efrain 239
     * @return int
240
     */
241
    public static function calculate_next_send_time(model $schedule, ?int $timenow = null): int {
242
        global $CFG;
243
 
1441 ariadna 244
        if ($timenow !== null) {
245
            debugging('Passing $timenow is deprecated, please use \core\clock dependency injection', DEBUG_DEVELOPER);
246
        }
1 efrain 247
 
1441 ariadna 248
        $timenow = di::get(clock::class)->time();
249
 
1 efrain 250
        $recurrence = $schedule->get('recurrence');
251
        $timescheduled = $schedule->get('timescheduled');
252
 
253
        // If no recurrence is set or we haven't reached last sent date, return early.
254
        if ($recurrence === model::RECURRENCE_NONE || $timescheduled > $timenow) {
255
            return $timescheduled;
256
        }
257
 
258
        // Extract attributes from date (year, month, day, hours, minutes).
259
        [
260
            'year' => $year,
261
            'mon' => $month,
262
            'mday' => $day,
263
            'wday' => $dayofweek,
264
            'hours' => $hour,
265
            'minutes' => $minute,
266
        ] = usergetdate($timescheduled, $CFG->timezone);
267
 
268
        switch ($recurrence) {
269
            case model::RECURRENCE_DAILY:
270
                $day += 1;
271
            break;
272
            case model::RECURRENCE_WEEKDAYS:
273
                $day += 1;
274
 
275
                $calendar = \core_calendar\type_factory::get_calendar_instance();
276
                $weekend = get_config('core', 'calendar_weekend');
277
 
278
                // Increment day until day of week falls on a weekday.
279
                while ((bool) ($weekend & (1 << (++$dayofweek % $calendar->get_num_weekdays())))) {
280
                    $day++;
281
                }
282
            break;
283
            case model::RECURRENCE_WEEKLY:
284
                $day += 7;
285
            break;
286
            case model::RECURRENCE_MONTHLY:
287
                $month += 1;
288
            break;
289
            case model::RECURRENCE_ANNUALLY:
290
                $year += 1;
291
            break;
292
        }
293
 
294
        // We need to recursively increment the timestamp until we get one after the current time.
295
        $timestamp = make_timestamp($year, $month, $day, $hour, $minute, 0, $CFG->timezone);
296
        if ($timestamp < $timenow) {
297
            // Ensure we don't modify anything in the original model.
298
            $scheduleclone = new model(0, $schedule->to_record());
299
 
1441 ariadna 300
            return self::calculate_next_send_time($scheduleclone->set('timescheduled', $timestamp));
1 efrain 301
        } else {
302
            return $timestamp;
303
        }
304
    }
305
 
306
    /**
307
     * Send schedule message to user
308
     *
309
     * @param model $schedule
310
     * @param stdClass $user
311
     * @param stored_file $attachment
312
     * @return bool
313
     */
314
    public static function send_schedule_message(model $schedule, stdClass $user, stored_file $attachment): bool {
315
        $message = new message();
316
        $message->component = 'moodle';
317
        $message->name = 'reportbuilderschedule';
318
        $message->courseid = SITEID;
319
        $message->userfrom = core_user::get_noreply_user();
320
        $message->userto = $user;
321
        $message->subject = $schedule->get('subject');
322
        $message->fullmessage = $schedule->get('message');
323
        $message->fullmessageformat = $schedule->get('messageformat');
324
        $message->fullmessagehtml = $message->fullmessage;
325
        $message->smallmessage = $message->fullmessage;
326
 
327
        // Attach report to outgoing message.
328
        $message->attachment = $attachment;
329
        $message->attachname = $attachment->get_filename();
330
 
331
        return (bool) message_send($message);
332
    }
333
 
334
    /**
335
     * Delete report schedule
336
     *
337
     * @param int $reportid
338
     * @param int $scheduleid
339
     * @return bool
340
     * @throws invalid_parameter_exception
341
     */
342
    public static function delete_schedule(int $reportid, int $scheduleid): bool {
343
        $schedule = model::get_record(['id' => $scheduleid, 'reportid' => $reportid]);
344
        if ($schedule === false) {
345
            throw new invalid_parameter_exception('Invalid schedule');
346
        }
347
 
348
        return $schedule->delete();
349
    }
350
 
351
    /**
352
     * Return list of available data formats
353
     *
354
     * @return string[]
355
     */
356
    public static function get_format_options(): array {
357
        $dataformats = dataformat::get_enabled_plugins();
358
 
359
        return array_map(static function(string $pluginname): string {
360
            return get_string('dataformat', 'dataformat_' . $pluginname);
361
        }, $dataformats);
362
    }
363
 
364
    /**
365
     * Return list of available view as user options
366
     *
367
     * @return string[]
368
     */
369
    public static function get_viewas_options(): array {
370
        return [
371
            model::REPORT_VIEWAS_CREATOR => get_string('scheduleviewascreator', 'core_reportbuilder'),
372
            model::REPORT_VIEWAS_RECIPIENT => get_string('scheduleviewasrecipient', 'core_reportbuilder'),
373
            model::REPORT_VIEWAS_USER => get_string('userselect', 'core_reportbuilder'),
374
        ];
375
    }
376
 
377
    /**
378
     * Return list of recurrence options
379
     *
380
     * @return string[]
381
     */
382
    public static function get_recurrence_options(): array {
383
        return [
384
            model::RECURRENCE_NONE => get_string('none'),
385
            model::RECURRENCE_DAILY => get_string('recurrencedaily', 'core_reportbuilder'),
386
            model::RECURRENCE_WEEKDAYS => get_string('recurrenceweekdays', 'core_reportbuilder'),
387
            model::RECURRENCE_WEEKLY => get_string('recurrenceweekly', 'core_reportbuilder'),
388
            model::RECURRENCE_MONTHLY => get_string('recurrencemonthly', 'core_reportbuilder'),
389
            model::RECURRENCE_ANNUALLY => get_string('recurrenceannually', 'core_reportbuilder'),
390
        ];
391
    }
392
 
393
    /**
394
     * Return list of options for when report is empty
395
     *
396
     * @return string[]
397
     */
398
    public static function get_report_empty_options(): array {
399
        return [
400
            model::REPORT_EMPTY_SEND_EMPTY => get_string('scheduleemptysendwithattachment', 'core_reportbuilder'),
401
            model::REPORT_EMPTY_SEND_WITHOUT => get_string('scheduleemptysendwithoutattachment', 'core_reportbuilder'),
402
            model::REPORT_EMPTY_DONT_SEND => get_string('scheduleemptydontsend', 'core_reportbuilder'),
403
        ];
404
    }
405
}