Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1441 ariadna 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
namespace core\output;
18
 
19
/**
20
 * Stored progress bar class.
21
 *
22
 * @package    core
23
 * @copyright  2023 onwards Catalyst IT {@link http://www.catalyst-eu.net/}
24
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25
 * @author     Conn Warwicker <conn.warwicker@catalyst-eu.net>
26
 */
27
class stored_progress_bar extends progress_bar {
28
 
29
    /** @var bool Can use output buffering. */
30
    protected static $supportsoutputbuffering = true;
31
 
32
    /** @var bool Flag to indicate the Javascript module has been initialised already. */
33
    protected static $jsloaded = false;
34
 
35
    /** @var int DB record ID */
36
    protected $recordid;
37
 
38
    /** @var string|null Message to associate with bar */
39
    protected $message = null;
40
 
41
    /** @var \core\clock Clock object */
42
    protected $clock;
43
 
44
    /**
45
     * This overwrites the progress_bar::__construct method.
46
     *
47
     * The stored progress bar does not need to check NO_OUTPUT_BUFFERING since it outputs to the page
48
     * then polls for updates asynchronously, rather than waiting for synchronous updates in later output.
49
     *
50
     * @param string $idnumber
51
     * @param int $width The suggested width.
52
     * @param bool $autostart Whether to start the progress bar right away.
53
     */
54
    public function __construct(string $idnumber, int $width = 0, bool $autostart = true) {
55
 
56
        $this->clock = \core\di::get(\core\clock::class);
57
 
58
        // Construct from the parent.
59
        parent::__construct($idnumber, $width, $autostart);
60
    }
61
 
62
    /**
63
     * Just set the timestart, do not render the bar immediately.
64
     *
65
     * @return void
66
     */
67
    public function create(): void {
68
        $this->timestart = $this->clock->time();
69
    }
70
 
71
    /**
72
     * Load the stored progress bar from the database based on its uniqued idnumber
73
     *
74
     * @param string $idnumber Unique ID of the bar
75
     * @return stored_progress_bar|null
76
     */
77
    public static function get_by_idnumber(string $idnumber): ?stored_progress_bar {
78
        global $DB;
79
 
80
        $record = $DB->get_record('stored_progress', ['idnumber' => $idnumber]);
81
        if ($record) {
82
            return self::load($record);
83
        } else {
84
            return null;
85
        }
86
    }
87
 
88
    /**
89
     * Load the stored progress bar from the database, based on it's record ID
90
     *
91
     * @param int $id Database record ID
92
     * @return stored_progress_bar|null
93
     */
94
    public static function get_by_id(int $id): ?stored_progress_bar {
95
        global $DB;
96
 
97
        $record = $DB->get_record('stored_progress', ['id' => $id]);
98
        if ($record) {
99
            return self::load($record);
100
        } else {
101
            return null;
102
        }
103
    }
104
 
105
    /**
106
     * Load the stored progress bar object from its record in the database.
107
     *
108
     * @param stdClass $record
109
     * @return stored_progress_bar
110
     */
111
    public static function load(\stdClass $record): stored_progress_bar {
112
        $progress = new stored_progress_bar($record->idnumber);
113
        $progress->set_record_id($record->id);
114
        $progress->set_time_started($record->timestart);
115
        $progress->set_last_updated($record->lastupdate);
116
        $progress->set_percent($record->percentcompleted);
117
        $progress->set_message($record->message);
118
        $progress->set_haserrored($record->haserrored);
119
        return $progress;
120
    }
121
 
122
    /**
123
     * Set the DB record ID
124
     *
125
     * @param int $id
126
     * @return void
127
     */
128
    protected function set_record_id(int $id): void {
129
        $this->recordid = $id;
130
    }
131
 
132
    /**
133
     * Set the time we started the process.
134
     *
135
     * @param ?int $value
136
     * @return void
137
     */
138
    protected function set_time_started(?int $value): void {
139
        $this->timestart = $value;
140
    }
141
 
142
    /**
143
     * Set the time we started last updated the progress.
144
     *
145
     * @param int|null $value
146
     * @return void
147
     */
148
    protected function set_last_updated(?int $value = null): void {
149
        $this->lastupdate = $value;
150
    }
151
 
152
    /**
153
     * Set the percent completed.
154
     *
155
     * @param float|null $value
156
     * @return void
157
     */
158
    protected function set_percent($value = null): void {
159
        $this->percent = $value;
160
    }
161
 
162
    /**
163
     * Set the message.
164
     *
165
     * @param string|null $value
166
     * @return void
167
     */
168
    protected function set_message(?string $value = null): void {
169
        $this->message = $value;
170
    }
171
 
172
    /**
173
     * Set that the process running has errored and store that against the bar
174
     *
175
     * @param string $errormsg
176
     * @return void
177
     */
178
    public function error(string $errormsg): void {
179
        // Update the error variables.
180
        parent::error($errormsg);
181
 
182
        // Update the record.
183
        $this->update_record();
184
    }
185
 
186
    /**
187
     * Get the progress bar message.
188
     *
189
     * @return string|null
190
     */
191
    public function get_message(): ?string {
192
        return $this->message;
193
    }
194
 
195
    /**
196
     * Initialise Javascript for stored progress bars.
197
     *
198
     * The javascript polls the status of all progress bars on the page, so it only needs to be initialised once.
199
     *
200
     * @return void
201
     */
202
    public function init_js(): void {
203
        global $PAGE;
204
        if (self::$jsloaded) {
205
            return;
206
        }
207
        $PAGE->requires->js_call_amd('core/stored_progress', 'init', [
208
            self::get_timeout(),
209
        ]);
210
        self::$jsloaded = true;
211
    }
212
 
213
    /**
214
     * Get the content to display the progress bar and start polling via AJAX
215
     *
216
     * @return string
217
     */
218
    public function get_content(): string {
219
        global $OUTPUT;
220
 
221
        $this->init_js();
222
 
223
        $context = $this->export_for_template($OUTPUT);
224
        return $OUTPUT->render_from_template('core/progress_bar', $context);
225
    }
226
 
227
    /**
228
     * Export for template.
229
     *
230
     * @param  renderer_base $output The renderer.
231
     * @return array
232
     */
233
    public function export_for_template(\renderer_base $output): array {
234
        $class = 'stored-progress-bar';
235
        if (empty($this->timestart)) {
236
            $class .= ' stored-progress-notstarted';
237
        }
238
        return [
239
            'id' => $this->recordid,
240
            'idnumber' => $this->idnumber,
241
            'width' => $this->width,
242
            'class' => $class,
243
            'value' => $this->percent,
244
            'message' => $this->message,
245
            'error' => $this->haserrored,
246
        ];
247
    }
248
 
249
    /**
250
     * Start the recording of the progress and store in the database
251
     *
252
     * @return int ID of the DB record
253
     */
254
    public function start(): int {
255
        global $OUTPUT, $DB;
256
 
257
        // If we are running in an non-interactive CLI environment, call the progress bar renderer to avoid warnings
258
        // when we do an update.
259
        if (defined('STDOUT') && !stream_isatty(STDOUT)) {
260
            $OUTPUT->render_progress_bar($this);
261
        }
262
 
263
        $record = $DB->get_record('stored_progress', ['idnumber' => $this->idnumber]);
264
        if ($record) {
265
            if ($record->timestart == 0) {
266
                // Set the timestart now and return.
267
                $record->timestart = $this->timestart;
268
                $DB->update_record('stored_progress', $record);
269
                $this->recordid = $record->id;
270
                return $this->recordid;
271
            } else {
272
                // Delete any existing records for this.
273
                $this->clear_records();
274
            }
275
        }
276
 
277
        // Create new progress record.
278
        $this->recordid = $DB->insert_record('stored_progress', [
279
            'idnumber' => $this->idnumber,
280
            'timestart' => (int)$this->timestart,
281
        ]);
282
 
283
        return $this->recordid;
284
    }
285
 
286
    /**
287
     * End the polling progress and delete the DB record.
288
     *
289
     * @return void
290
     */
291
    protected function clear_records(): void {
292
        global $DB;
293
 
294
        $DB->delete_records('stored_progress', [
295
            'idnumber' => $this->idnumber,
296
        ]);
297
    }
298
 
299
    /**
300
     * Update the database record with the percentage and message
301
     *
302
     * @param float $percent
303
     * @param string $msg
304
     * @return void
305
     */
306
    protected function update_raw($percent, $msg): void {
307
        $this->percent = $percent;
308
        $this->message = $msg;
309
 
310
        // Update the database record with the new data.
311
        $this->update_record();
312
 
313
        // Update any CLI script's progress with an ASCII progress bar.
314
        $this->render_update();
315
    }
316
 
317
    /**
318
     * Render an update to the CLI
319
     *
320
     * This will only work in CLI scripts, and not in scheduled/adhoc tasks even though they run via CLI,
321
     * as they seem to use a different renderer (core_renderer instead of core_renderer_cli).
322
     *
323
     * We also can't check this based on "CLI_SCRIPT" const as that is true for tasks.
324
     *
325
     * So this will just check a flag to see if we want auto rendering of updates.
326
     *
327
     * @return void
328
     */
329
    protected function render_update(): void {
330
        global $OUTPUT;
331
 
332
        // If no output buffering, don't render it at all.
333
        if (defined('NO_OUTPUT_BUFFERING') && NO_OUTPUT_BUFFERING) {
334
            $this->auto_update(false);
335
        }
336
 
337
        // If we want the screen to auto update, render it.
338
        if ($this->autoupdate) {
339
            echo $OUTPUT->render_progress_bar_update(
340
                $this->idnumber, $this->percent, $this->message, $this->get_estimate_message($this->percent)
341
            );
342
        }
343
    }
344
 
345
    /**
346
     * Update the database record
347
     *
348
     * @throws \moodle_exception
349
     * @return void
350
     */
351
    protected function update_record(): void {
352
        global $DB;
353
 
354
        if (is_null($this->recordid)) {
355
            throw new \moodle_exception('Polling has not been started. Cannot set iteration.');
356
        }
357
 
358
        // Update time.
359
        $this->lastupdate = $this->clock->time();
360
 
361
        // Update the database record.
362
        $record = new \stdClass();
363
        $record->id = $this->recordid;
364
        $record->lastupdate = (int)$this->lastupdate;
365
        $record->percentcompleted = $this->percent;
366
        $record->message = $this->message;
367
        $record->haserrored = $this->haserrored;
368
        $DB->update_record('stored_progress', $record);
369
    }
370
 
371
    /**
372
     * We need a way to specify a unique idnumber for processes being monitored, so that
373
     * firstly we don't accidentally overwrite a running process, and secondly so we can
374
     * automatically load them in some cases, without having to manually code in its name.
375
     *
376
     * So this uses the classname of the object being monitored, along with its id.
377
     *
378
     * This method should be used when creating the stored_progress record to set it's idnumber.
379
     *
380
     * @param string $class Class name of the object being monitored, e.g. \local_something\task\my_task
381
     * @param int|null $id ID of an object from database, e.g. 123
382
     * @return string Converted string, e.g. local_something_task_my_task_123
383
     */
384
    public static function convert_to_idnumber(string $class, ?int $id = null): string {
385
        $idnumber = preg_replace("/[^a-z0-9_]/", "_", ltrim($class, '\\'));
386
        if (!is_null($id)) {
387
            $idnumber .= '_' . $id;
388
        }
389
 
390
        return $idnumber;
391
    }
392
 
393
    /**
394
     * Get the polling timeout in seconds. Default: 5.
395
     *
396
     * @return int
397
     */
398
    public static function get_timeout(): int {
399
        global $CFG;
400
        return $CFG->progresspollinterval ?? 5;
401
    }
402
 
403
    /**
404
     * Store a progress bar record in a pending state.
405
     *
406
     * @return int ID of the DB record
407
     */
408
    public function store_pending(): int {
409
        global $DB;
410
 
411
        // Delete any existing records for this.
412
        $this->clear_records();
413
 
414
        // Create new progress record.
415
        $this->recordid = $DB->insert_record('stored_progress', [
416
            'idnumber' => $this->idnumber,
417
            'timestart' => $this->timestart,
418
            'message' => '',
419
        ]);
420
 
421
        return $this->recordid;
422
    }
423
 
424
}