Proyectos de Subversion Moodle

Rev

Ir a la última revisión | | 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_course\reportbuilder\local\entities;
20
 
21
use core_reportbuilder\local\entities\base;
22
use core_course\reportbuilder\local\formatters\completion as completion_formatter;
23
use core_reportbuilder\local\filters\boolean_select;
24
use core_reportbuilder\local\filters\date;
25
use core_reportbuilder\local\helpers\database;
26
use core_reportbuilder\local\helpers\format;
27
use core_reportbuilder\local\report\column;
28
use core_reportbuilder\local\report\filter;
29
use completion_criteria_completion;
30
use completion_info;
31
use html_writer;
32
use lang_string;
33
use stdClass;
34
 
35
/**
36
 * Course completion entity implementation
37
 *
38
 * @package     core_course
39
 * @copyright   2022 David Matamoros <davidmc@moodle.com>
40
 * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
41
 */
42
class completion extends base {
43
 
44
    /**
45
     * Database tables that this entity uses
46
     *
47
     * @return string[]
48
     */
49
    protected function get_default_tables(): array {
50
        return [
51
            'course_completion',
52
            'course',
53
            'grade_grades' ,
54
            'grade_items',
55
            'user',
56
        ];
57
    }
58
 
59
    /**
60
     * The default title for this entity in the list of columns/conditions/filters in the report builder
61
     *
62
     * @return lang_string
63
     */
64
    protected function get_default_entity_title(): lang_string {
65
        return new lang_string('coursecompletion', 'completion');
66
    }
67
 
68
    /**
69
     * Initialise the entity
70
     *
71
     * @return base
72
     */
73
    public function initialise(): base {
74
        foreach ($this->get_all_columns() as $column) {
75
            $this->add_column($column);
76
        }
77
 
78
        // All the filters defined by the entity can also be used as conditions.
79
        foreach ($this->get_all_filters() as $filter) {
80
            $this
81
                ->add_filter($filter)
82
                ->add_condition($filter);
83
        }
84
 
85
        return $this;
86
    }
87
 
88
    /**
89
     * Returns list of all available columns
90
     *
91
     * @return column[]
92
     */
93
    protected function get_all_columns(): array {
94
        [
95
            'course_completion' => $coursecompletion,
96
            'course' => $course,
97
            'grade_grades' => $grade,
98
            'grade_items' => $gradeitem,
99
            'user' => $user,
100
        ] = $this->get_table_aliases();
101
 
102
        // Completed column.
103
        $columns[] = (new column(
104
            'completed',
105
            new lang_string('completed', 'completion'),
106
            $this->get_entity_name()
107
        ))
108
            ->add_joins($this->get_joins())
109
            ->set_type(column::TYPE_BOOLEAN)
110
            ->add_field("CASE WHEN {$coursecompletion}.timecompleted > 0 THEN 1 ELSE 0 END", 'completed')
111
            ->add_field("{$user}.id", 'userid')
112
            ->set_is_sortable(true)
113
            ->add_callback(static function(bool $value, stdClass $row): string {
114
                if (!$row->userid) {
115
                    return '';
116
                }
117
                return format::boolean_as_text($value);
118
            });
119
 
120
        // Completion criteria column.
121
        $criterias = database::generate_alias();
122
        $columns[] = (new column(
123
            'criteria',
124
            new lang_string('criteria', 'core_completion'),
125
            $this->get_entity_name()
126
        ))
127
            ->add_joins($this->get_joins())
128
            // Determine whether any criteria exist for the course. We also group per course, rather than report each separately.
129
            ->add_join("LEFT JOIN (
130
                            SELECT DISTINCT course FROM {course_completion_criteria}
131
                       ) {$criterias} ON {$criterias}.course = {$course}.id")
132
            ->set_type(column::TYPE_TEXT)
133
            // Select enough fields to determine user criteria for the course.
134
            ->add_field("{$criterias}.course", 'courseid')
135
            ->add_field("{$course}.enablecompletion")
136
            ->add_field("{$user}.id", 'userid')
137
            ->set_disabled_aggregation_all()
138
            ->add_callback(static function($id, stdClass $record): string {
139
                if (!$record->courseid) {
140
                    return '';
141
                }
142
 
143
                $info = new completion_info((object) ['id' => $record->courseid, 'enablecompletion' => $record->enablecompletion]);
144
                if ($info->get_aggregation_method() == COMPLETION_AGGREGATION_ALL) {
145
                    $title = get_string('criteriarequiredall', 'core_completion');
146
                } else {
147
                    $title = get_string('criteriarequiredany', 'core_completion');
148
                }
149
 
150
                // Map all completion data to their criteria summaries.
151
                $items = array_map(static function(completion_criteria_completion $completion): string {
152
                    $criteria = $completion->get_criteria();
153
 
154
                    return get_string('criteriasummary', 'core_completion', [
155
                        'type' => $criteria->get_details($completion)['type'],
156
                        'summary' => $criteria->get_title_detailed(),
157
                    ]);
158
                }, $info->get_completions((int) $record->userid));
159
 
160
                return $title . html_writer::alist($items);
161
            });
162
 
163
        // Progress percentage column.
164
        $columns[] = (new column(
165
            'progresspercent',
166
            new lang_string('progress', 'completion'),
167
            $this->get_entity_name()
168
        ))
169
            ->add_joins($this->get_joins())
170
            ->set_type(column::TYPE_TEXT)
171
            ->add_field("{$course}.id", 'courseid')
172
            ->add_field("{$user}.id", 'userid')
173
            ->set_is_sortable(false)
174
            ->add_callback([completion_formatter::class, 'completion_progress']);
175
 
176
        // Time enrolled.
177
        $columns[] = (new column(
178
            'timeenrolled',
179
            new lang_string('timeenrolled', 'enrol'),
180
            $this->get_entity_name()
181
        ))
182
            ->add_joins($this->get_joins())
183
            ->set_type(column::TYPE_TIMESTAMP)
184
            ->add_field("{$coursecompletion}.timeenrolled")
185
            ->set_is_sortable(true)
186
            ->add_callback([format::class, 'userdate']);
187
 
188
        // Time started.
189
        $columns[] = (new column(
190
            'timestarted',
191
            new lang_string('timestarted', 'enrol'),
192
            $this->get_entity_name()
193
        ))
194
            ->add_joins($this->get_joins())
195
            ->set_type(column::TYPE_TIMESTAMP)
196
            ->add_field("{$coursecompletion}.timestarted")
197
            ->set_is_sortable(true)
198
            ->add_callback([format::class, 'userdate']);
199
 
200
        // Time completed.
201
        $columns[] = (new column(
202
            'timecompleted',
203
            new lang_string('timecompleted', 'completion'),
204
            $this->get_entity_name()
205
        ))
206
            ->add_joins($this->get_joins())
207
            ->set_type(column::TYPE_TIMESTAMP)
208
            ->add_field("{$coursecompletion}.timecompleted")
209
            ->set_is_sortable(true)
210
            ->add_callback([format::class, 'userdate']);
211
 
212
        // Time reaggregated.
213
        $columns[] = (new column(
214
            'reaggregate',
215
            new lang_string('timereaggregated', 'enrol'),
216
            $this->get_entity_name()
217
        ))
218
            ->add_joins($this->get_joins())
219
            ->set_type(column::TYPE_TIMESTAMP)
220
            ->add_field("{$coursecompletion}.reaggregate")
221
            ->set_is_sortable(true)
222
            ->add_callback([format::class, 'userdate']);
223
 
224
        // Days taking course (days since course start date until completion or until current date if not completed).
225
        $currenttime = time();
226
        $columns[] = (new column(
227
            'dayscourse',
228
            new lang_string('daystakingcourse', 'course'),
229
            $this->get_entity_name()
230
        ))
231
            ->add_joins($this->get_joins())
232
            ->set_type(column::TYPE_INTEGER)
233
            ->add_field("(
234
                CASE
235
                    WHEN {$coursecompletion}.timecompleted > 0 THEN
236
                        {$coursecompletion}.timecompleted
237
                    ELSE
238
                        {$currenttime}
239
                END - {$course}.startdate) / " . DAYSECS, 'dayscourse')
240
            ->add_field("{$user}.id", 'userid')
241
            ->set_is_sortable(true)
242
            ->add_callback([completion_formatter::class, 'get_days']);
243
 
244
        // Days since last completion (days since last enrolment date until completion or until current date if not completed).
245
        $columns[] = (new column(
246
            'daysuntilcompletion',
247
            new lang_string('daysuntilcompletion', 'completion'),
248
            $this->get_entity_name()
249
        ))
250
            ->add_joins($this->get_joins())
251
            ->set_type(column::TYPE_INTEGER)
252
            ->add_field("(
253
                CASE
254
                    WHEN {$coursecompletion}.timecompleted > 0 THEN
255
                        {$coursecompletion}.timecompleted
256
                    ELSE
257
                        {$currenttime}
258
                END - {$coursecompletion}.timeenrolled) / " . DAYSECS, 'daysuntilcompletion')
259
            ->add_field("{$user}.id", 'userid')
260
            ->set_is_sortable(true)
261
            ->add_callback([completion_formatter::class, 'get_days']);
262
 
263
        // Student course grade.
264
        $columns[] = (new column(
265
            'grade',
266
            new lang_string('gradenoun'),
267
            $this->get_entity_name()
268
        ))
269
            ->add_joins($this->get_joins())
270
            ->add_join("
271
                LEFT JOIN {grade_items} {$gradeitem}
272
                       ON ({$gradeitem}.itemtype = 'course' AND {$course}.id = {$gradeitem}.courseid)
273
            ")
274
            ->add_join("
275
                LEFT JOIN {grade_grades} {$grade}
276
                       ON ({$user}.id = {$grade}.userid AND {$gradeitem}.id = {$grade}.itemid)
277
            ")
278
            ->set_type(column::TYPE_FLOAT)
279
            ->add_fields("{$grade}.finalgrade")
280
            ->set_is_sortable(true)
281
            ->add_callback(function(?float $value): string {
282
                if ($value === null) {
283
                    return '';
284
                }
285
                return format_float($value, 2);
286
            });
287
 
288
        return $columns;
289
    }
290
 
291
    /**
292
     * Return list of all available filters
293
     *
294
     * @return filter[]
295
     */
296
    protected function get_all_filters(): array {
297
        $coursecompletion = $this->get_table_alias('course_completion');
298
 
299
        // Completed status filter.
300
        $filters[] = (new filter(
301
            boolean_select::class,
302
            'completed',
303
            new lang_string('completed', 'completion'),
304
            $this->get_entity_name(),
305
            "CASE WHEN {$coursecompletion}.timecompleted > 0 THEN 1 ELSE 0 END"
306
        ))
307
            ->add_joins($this->get_joins());
308
 
309
        // Time completed filter.
310
        $filters[] = (new filter(
311
            date::class,
312
            'timecompleted',
313
            new lang_string('timecompleted', 'completion'),
314
            $this->get_entity_name(),
315
            "{$coursecompletion}.timecompleted"
316
        ))
317
            ->add_joins($this->get_joins())
318
            ->set_limited_operators([
319
                date::DATE_ANY,
320
                date::DATE_NOT_EMPTY,
321
                date::DATE_EMPTY,
322
                date::DATE_RANGE,
323
                date::DATE_LAST,
324
                date::DATE_CURRENT,
325
            ]);
326
 
327
        // Time enrolled/started filter and condition.
328
        $fields = ['timeenrolled', 'timestarted'];
329
        foreach ($fields as $field) {
330
            $filters[] = (new filter(
331
                date::class,
332
                $field,
333
                new lang_string($field, 'enrol'),
334
                $this->get_entity_name(),
335
                "{$coursecompletion}.{$field}"
336
            ))
337
                ->add_joins($this->get_joins())
338
                ->set_limited_operators([
339
                    date::DATE_ANY,
340
                    date::DATE_NOT_EMPTY,
341
                    date::DATE_EMPTY,
342
                    date::DATE_RANGE,
343
                    date::DATE_LAST,
344
                    date::DATE_CURRENT,
345
                ]);
346
        }
347
 
348
        // Time reaggregated filter and condition.
349
        $filters[] = (new filter(
350
            date::class,
351
            'reaggregate',
352
            new lang_string('timereaggregated', 'enrol'),
353
            $this->get_entity_name(),
354
            "{$coursecompletion}.reaggregate"
355
        ))
356
            ->add_joins($this->get_joins())
357
            ->set_limited_operators([
358
                date::DATE_ANY,
359
                date::DATE_NOT_EMPTY,
360
                date::DATE_EMPTY,
361
                date::DATE_RANGE,
362
                date::DATE_LAST,
363
                date::DATE_CURRENT,
364
            ]);
365
 
366
        return $filters;
367
    }
368
}