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
 * Renderable class for gradehistory report.
19
 *
20
 * @package    gradereport_history
21
 * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
22
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 */
24
 
25
namespace gradereport_history\output;
26
 
27
defined('MOODLE_INTERNAL') || die;
28
 
29
require_once($CFG->libdir . '/tablelib.php');
30
require_once($CFG->dirroot . '/user/lib.php');
31
 
32
/**
33
 * Renderable class for gradehistory report.
34
 *
35
 * @since      Moodle 2.8
36
 * @package    gradereport_history
37
 * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
38
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
39
 */
40
class tablelog extends \table_sql implements \renderable {
41
 
42
    /**
43
     * @var int course id.
44
     */
45
    protected $courseid;
46
 
47
    /**
48
     * @var \context context of the page to be rendered.
49
     */
50
    protected $context;
51
 
52
    /**
53
     * @var \stdClass A list of filters to be applied to the sql query.
54
     */
55
    protected $filters;
56
 
57
    /**
58
     * @var \stdClass[] List of users included in the report (if userids are specified as filters)
59
     */
60
    protected $users = [];
61
 
62
    /**
63
     * @var array A list of grade items present in the course.
64
     */
65
    protected $gradeitems = array();
66
 
67
    /**
68
     * @var \course_modinfo|null A list of cm instances in course.
69
     */
70
    protected $cms;
71
 
72
    /**
73
     * @var int The default number of decimal points to use in this course
74
     * when a grade item does not itself define the number of decimal points.
75
     */
76
    protected $defaultdecimalpoints;
77
 
78
    /**
79
     * Sets up the table_log parameters.
80
     *
81
     * @param string $uniqueid unique id of table.
82
     * @param \context_course $context Context of the report.
83
     * @param \moodle_url $url url of the page where this table would be displayed.
84
     * @param array $filters options are:
85
     *                          userids : limit to specific users (default: none)
86
     *                          itemid : limit to specific grade item (default: all)
87
     *                          grader : limit to specific graders (default: all)
88
     *                          datefrom : start of date range
89
     *                          datetill : end of date range
90
     *                          revisedonly : only show revised grades (default: false)
91
     *                          format : page | csv | excel (default: page)
92
     * @param string $download Represents download format, pass '' no download at this time.
93
     * @param int $page The current page being displayed.
94
     * @param int $perpage Number of rules to display per page.
95
     */
96
    public function __construct($uniqueid, \context_course $context, $url, $filters = array(), $download = '', $page = 0,
97
                                $perpage = 100) {
98
        global $CFG;
99
        parent::__construct($uniqueid);
100
 
101
        $this->set_attribute('class', 'gradereport_history generaltable generalbox');
102
 
103
        // Set protected properties.
104
        $this->context = $context;
105
        $this->courseid = $this->context->instanceid;
106
        $this->pagesize = $perpage;
107
        $this->currpage = $page;
108
        $this->gradeitems = \grade_item::fetch_all(array('courseid' => $this->courseid));
109
        $this->cms = get_fast_modinfo($this->courseid);
110
        $this->useridfield = 'userid';
111
        $this->defaultdecimalpoints = grade_get_setting($this->courseid, 'decimalpoints', $CFG->grade_decimalpoints);
112
 
113
        // Define columns in the table.
114
        $this->define_table_columns();
115
 
116
        // Define filters.
117
        $this->define_table_filters((object) $filters);
118
 
119
        // Define configs.
120
        $this->define_table_configs($url);
121
 
122
        // Set download status.
123
        $this->is_downloading($download, get_string('exportfilename', 'gradereport_history'));
124
    }
125
 
126
    /**
127
     * Define table configs.
128
     *
129
     * @param \moodle_url $url url of the page where this table would be displayed.
130
     */
131
    protected function define_table_configs(\moodle_url $url) {
132
 
133
        // Set table url.
134
        $urlparams = (array)$this->filters;
135
        unset($urlparams['submitbutton']);
136
        unset($urlparams['userfullnames']);
137
        $url->params($urlparams);
138
        $this->define_baseurl($url);
139
 
140
        // Set table configs.
141
        $this->collapsible(true);
142
        $this->sortable(true, 'timemodified', SORT_DESC);
143
        $this->pageable(true);
144
        $this->no_sorting('grader');
145
    }
146
 
147
    /**
148
     * Define table filters
149
     *
150
     * @param \stdClass $filters
151
     */
152
    protected function define_table_filters(\stdClass $filters): void {
153
        global $DB;
154
 
155
        $this->filters = $filters;
156
 
157
        if (!empty($this->filters->userids)) {
158
 
159
            $course = get_course($this->courseid);
160
 
161
            // Retrieve userids that are part of the filters object, and ensure user can access each of them.
162
            [$userselect, $userparams] = $DB->get_in_or_equal(explode(',', $this->filters->userids), SQL_PARAMS_NAMED);
163
            [$usersort] = users_order_by_sql();
164
 
165
            $this->users = array_filter(
166
                $DB->get_records_select('user', "id {$userselect}", $userparams, $usersort),
167
                static function(\stdClass $user) use ($course): bool {
168
                    return user_can_view_profile($user, $course);
169
                }
170
            );
171
 
172
            // Reset userids to the filtered array of users.
173
            $this->filters->userids = implode(',', array_keys($this->users));
174
        }
175
    }
176
 
177
    /**
178
     * Setup the headers for the html table.
179
     */
180
    protected function define_table_columns() {
181
        $extrafields = \core_user\fields::get_identity_fields($this->context);
182
 
183
        // Define headers and columns.
184
        $cols = array(
185
            'timemodified' => get_string('datetime', 'gradereport_history'),
186
            'fullname' => get_string('name')
187
        );
188
 
189
        // Add headers for extra user fields.
190
        foreach ($extrafields as $field) {
191
            if (get_string_manager()->string_exists($field, 'moodle')) {
192
                $cols[$field] = get_string($field);
193
            } else {
194
                $cols[$field] = \core_user\fields::get_display_name($field);
195
            }
196
        }
197
 
198
        // Add remaining headers.
199
        $cols = array_merge($cols, array(
200
            'itemname' => get_string('gradeitem', 'grades'),
201
            'prevgrade' => get_string('gradeold', 'gradereport_history'),
202
            'finalgrade' => get_string('gradenew', 'gradereport_history'),
203
            'grader' => get_string('grader', 'gradereport_history'),
204
            'source' => get_string('source', 'gradereport_history'),
205
            'overridden' => get_string('overridden', 'grades'),
206
            'locked' => get_string('locked', 'grades'),
207
            'excluded' => get_string('excluded', 'gradereport_history'),
208
            'feedback' => get_string('feedbacktext', 'gradereport_history')
209
            )
210
        );
211
 
212
        $this->define_columns(array_keys($cols));
213
        $this->define_headers(array_values($cols));
214
    }
215
 
216
    /**
217
     * Method to display the final grade.
218
     *
219
     * @param \stdClass $history an entry of history record.
220
     *
221
     * @return string HTML to display
222
     */
223
    public function col_finalgrade(\stdClass $history) {
224
        if (!empty($this->gradeitems[$history->itemid])) {
225
            $decimalpoints = $this->gradeitems[$history->itemid]->get_decimals();
226
        } else {
227
            $decimalpoints = $this->defaultdecimalpoints;
228
        }
229
 
230
        return format_float($history->finalgrade, $decimalpoints);
231
    }
232
 
233
    /**
234
     * Method to display the previous grade.
235
     *
236
     * @param \stdClass $history an entry of history record.
237
     *
238
     * @return string HTML to display
239
     */
240
    public function col_prevgrade(\stdClass $history) {
241
        if (!empty($this->gradeitems[$history->itemid])) {
242
            $decimalpoints = $this->gradeitems[$history->itemid]->get_decimals();
243
        } else {
244
            $decimalpoints = $this->defaultdecimalpoints;
245
        }
246
 
247
        return format_float($history->prevgrade, $decimalpoints);
248
    }
249
 
250
    /**
251
     * Method to display column timemodifed.
252
     *
253
     * @param \stdClass $history an entry of history record.
254
     *
255
     * @return string HTML to display
256
     */
257
    public function col_timemodified(\stdClass $history) {
258
        return userdate($history->timemodified);
259
    }
260
 
261
    /**
262
     * Method to display column itemname.
263
     *
264
     * @param \stdClass $history an entry of history record.
265
     *
266
     * @return string HTML to display
267
     */
268
    public function col_itemname(\stdClass $history) {
269
        // Make sure grade item is still present and link it to the module if possible.
270
        $itemid = $history->itemid;
271
        if (!empty($this->gradeitems[$itemid])) {
272
            if ($history->itemtype === 'mod' && !$this->is_downloading()) {
273
                if (!empty($this->cms->instances[$history->itemmodule][$history->iteminstance])) {
274
                    $cm = $this->cms->instances[$history->itemmodule][$history->iteminstance];
275
                    $url = new \moodle_url('/mod/' . $history->itemmodule . '/view.php', array('id' => $cm->id));
276
                    return \html_writer::link($url, $this->gradeitems[$itemid]->get_name());
277
                }
278
            }
279
            return $this->gradeitems[$itemid]->get_name();
280
        }
281
        return get_string('deleteditemid', 'gradereport_history', $history->itemid);
282
    }
283
 
284
    /**
285
     * Method to display column grader.
286
     *
287
     * @param \stdClass $history an entry of history record.
288
     *
289
     * @return string HTML to display
290
     */
291
    public function col_grader(\stdClass $history) {
292
        if (empty($history->usermodified)) {
293
            // Not every row has a valid usermodified.
294
            return '';
295
        }
296
 
297
        $grader = new \stdClass();
298
        $grader = username_load_fields_from_object($grader, $history, 'grader');
299
        $name = fullname($grader);
300
 
301
        if ($this->download) {
302
            return $name;
303
        }
304
 
305
        $userid = $history->usermodified;
306
        $profileurl = new \moodle_url('/user/view.php', array('id' => $userid, 'course' => $this->courseid));
307
 
308
        return \html_writer::link($profileurl, $name);
309
    }
310
 
311
    /**
312
     * Method to display column overridden.
313
     *
314
     * @param \stdClass $history an entry of history record.
315
     *
316
     * @return string HTML to display
317
     */
318
    public function col_overridden(\stdClass $history) {
319
        return $history->overridden ? get_string('yes') : get_string('no');
320
    }
321
 
322
    /**
323
     * Method to display column locked.
324
     *
325
     * @param \stdClass $history an entry of history record.
326
     *
327
     * @return string HTML to display
328
     */
329
    public function col_locked(\stdClass $history) {
330
        return $history->locked ? get_string('yes') : get_string('no');
331
    }
332
 
333
    /**
334
     * Method to display column excluded.
335
     *
336
     * @param \stdClass $history an entry of history record.
337
     *
338
     * @return string HTML to display
339
     */
340
    public function col_excluded(\stdClass $history) {
341
        return $history->excluded ? get_string('yes') : get_string('no');
342
    }
343
 
344
    /**
345
     * Method to display column feedback.
346
     *
347
     * @param \stdClass $history an entry of history record.
348
     *
349
     * @return string HTML to display
350
     */
351
    public function col_feedback(\stdClass $history) {
352
        if ($this->is_downloading()) {
353
            return $history->feedback;
354
        } else {
355
            // We need the activity context, not the course context.
356
            $gradeitem = $this->gradeitems[$history->itemid];
357
            $context = $gradeitem->get_context();
358
 
359
            $feedback = file_rewrite_pluginfile_urls(
360
                $history->feedback,
361
                'pluginfile.php',
362
                $context->id,
363
                GRADE_FILE_COMPONENT,
364
                GRADE_HISTORY_FEEDBACK_FILEAREA,
365
                $history->id
366
            );
367
 
368
            return format_text($feedback, $history->feedbackformat, array('context' => $context));
369
        }
370
    }
371
 
372
    /**
373
     * Builds the sql and param list needed, based on the user selected filters.
374
     *
375
     * @return array containing sql to use and an array of params.
376
     */
377
    protected function get_filters_sql_and_params() {
378
        global $DB, $USER;
379
 
380
        $coursecontext = $this->context;
381
        $filter = 'gi.courseid = :courseid';
382
        $params = array(
383
            'courseid' => $coursecontext->instanceid,
384
        );
385
 
386
        if (!empty($this->filters->itemid)) {
387
            $filter .= ' AND ggh.itemid = :itemid';
388
            $params['itemid'] = $this->filters->itemid;
389
        }
390
        if (!empty($this->filters->userids)) {
391
            $list = explode(',', $this->filters->userids);
392
            list($insql, $plist) = $DB->get_in_or_equal($list, SQL_PARAMS_NAMED);
393
            $filter .= " AND ggh.userid $insql";
394
            $params += $plist;
395
        }
396
        if (!empty($this->filters->datefrom)) {
397
            $filter .= " AND ggh.timemodified >= :datefrom";
398
            $params += array('datefrom' => $this->filters->datefrom);
399
        }
400
        if (!empty($this->filters->datetill)) {
401
            $filter .= " AND ggh.timemodified <= :datetill";
402
            $params += array('datetill' => $this->filters->datetill);
403
        }
404
        if (!empty($this->filters->grader)) {
405
            $filter .= " AND ggh.usermodified = :grader";
406
            $params += array('grader' => $this->filters->grader);
407
        }
408
 
409
        // If the course is separate group mode and the current user is not allowed to see all groups make sure
410
        // that we display only users from the same groups as current user.
411
        $groupmode = get_course($coursecontext->instanceid)->groupmode;
412
        if ($groupmode == SEPARATEGROUPS && !has_capability('moodle/site:accessallgroups', $coursecontext)) {
413
            $groupids = array_column(groups_get_all_groups($coursecontext->instanceid, $USER->id, 0, 'g.id'), 'id');
414
            list($gsql, $gparams) = $DB->get_in_or_equal($groupids, SQL_PARAMS_NAMED, 'gmuparam', true, 0);
415
            $filter .= " AND EXISTS (SELECT 1 FROM {groups_members} gmu WHERE gmu.userid=ggh.userid AND gmu.groupid $gsql)";
416
            $params += $gparams;
417
        }
418
 
419
        return array($filter, $params);
420
    }
421
 
422
    /**
423
     * Builds the complete sql with all the joins to get the grade history data.
424
     *
425
     * @param bool $count setting this to true, returns an sql to get count only instead of the complete data records.
426
     *
427
     * @return array containing sql to use and an array of params.
428
     */
429
    protected function get_sql_and_params($count = false) {
430
        $fields = 'ggh.id, ggh.timemodified, ggh.itemid, ggh.userid, ggh.finalgrade, ggh.usermodified,
431
                   ggh.source, ggh.overridden, ggh.locked, ggh.excluded, ggh.feedback, ggh.feedbackformat,
432
                   gi.itemtype, gi.itemmodule, gi.iteminstance, gi.itemnumber, ';
433
 
434
        $userfieldsapi = \core_user\fields::for_identity($this->context);
435
        $userfieldssql = $userfieldsapi->get_sql('u', true, '', '', true);
436
        $userfieldsselects = '';
437
        $userfieldsjoins = '';
438
        $userfieldsparams = [];
439
        if (!$count) {
440
            $userfieldsselects = $userfieldssql->selects;
441
            $userfieldsjoins = $userfieldssql->joins;
442
            $userfieldsparams = $userfieldssql->params;
443
        }
444
 
445
        // Add extra user fields that we need for the graded user.
446
        $extrafields = [];
447
        foreach ($userfieldsapi->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]) as $field) {
448
            $extrafields[$field] = $userfieldssql->mappings[$field];
449
        }
450
        $userfieldsapi = \core_user\fields::for_name();
451
        $fields .= $userfieldsapi->get_sql('u', false, '', '', false)->selects . ', ';
452
        $groupby = $fields;
453
 
454
        // Add extra user fields that we need for the grader user.
455
        $fields .= $userfieldsapi->get_sql('ug', false, 'grader', '', false)->selects;
456
        $groupby .= $userfieldsapi->get_sql('ug', false, '', '', false)->selects;
457
 
458
        // Filtering on revised grades only.
459
        $revisedonly = !empty($this->filters->revisedonly);
460
 
461
        if ($count && !$revisedonly) {
462
            // We can only directly use count when not using the filter revised only.
463
            $select = "COUNT(1)";
464
        } else {
465
            // Fetching the previous grade. We use MAX() to ensure that we only get one result if
466
            // more than one histories happened at the same second.
467
            $prevgrade = "SELECT MAX(finalgrade)
468
                            FROM {grade_grades_history} h
469
                           WHERE h.itemid = ggh.itemid
470
                             AND h.userid = ggh.userid
471
                             AND h.timemodified < ggh.timemodified
472
                             AND NOT EXISTS (
473
                              SELECT 1
474
                                FROM {grade_grades_history} h2
475
                               WHERE h2.itemid = ggh.itemid
476
                                 AND h2.userid = ggh.userid
477
                                 AND h2.timemodified < ggh.timemodified
478
                                 AND h.timemodified < h2.timemodified)";
479
 
480
            $select = "$fields, ($prevgrade) AS prevgrade,
481
                      CASE WHEN gi.itemname IS NULL THEN gi.itemtype ELSE gi.itemname END AS itemname";
482
        }
483
 
484
        list($where, $params) = $this->get_filters_sql_and_params();
485
 
486
        $sql = " SELECT $select $userfieldsselects
487
                   FROM {grade_grades_history} ggh
488
                   JOIN {grade_items} gi ON gi.id = ggh.itemid
489
                   JOIN {user} u ON u.id = ggh.userid
490
                        $userfieldsjoins
491
              LEFT JOIN {user} ug ON ug.id = ggh.usermodified
492
                  WHERE $where";
493
        $params = array_merge($userfieldsparams, $params);
494
 
495
        // As prevgrade is a dynamic field, we need to wrap the query. This is the only filtering
496
        // that should be defined outside the method self::get_filters_sql_and_params().
497
        if ($revisedonly) {
498
            $allorcount = $count ? 'COUNT(1)' : '*';
499
            $sql = "SELECT $allorcount FROM ($sql) pg
500
                     WHERE pg.finalgrade != pg.prevgrade
501
                        OR (pg.prevgrade IS NULL AND pg.finalgrade IS NOT NULL)
502
                        OR (pg.prevgrade IS NOT NULL AND pg.finalgrade IS NULL)";
503
        }
504
 
505
        // Add order by if needed.
506
        if (!$count && $sqlsort = $this->get_sql_sort()) {
507
            $sql .= " ORDER BY " . $sqlsort;
508
        }
509
 
510
        return array($sql, $params);
511
    }
512
 
513
    /**
514
     * Get the SQL fragment to sort by.
515
     *
516
     * This is overridden to sort by timemodified and ID by default. Many items happen at the same time
517
     * and a second sorting by ID is valuable to distinguish the order in which the history happened.
518
     *
519
     * @return string SQL fragment.
520
     */
521
    public function get_sql_sort() {
522
        $columns = $this->get_sort_columns();
523
        if (count($columns) == 1 && isset($columns['timemodified']) && $columns['timemodified'] == SORT_DESC) {
524
            // Add the 'id' column when we are using the default sorting.
525
            $columns['id'] = SORT_DESC;
526
            return self::construct_order_by($columns);
527
        }
528
        return parent::get_sql_sort();
529
    }
530
 
531
    /**
532
     * Query the reader. Store results in the object for use by build_table.
533
     *
534
     * @param int $pagesize size of page for paginated displayed table.
535
     * @param bool $useinitialsbar do you want to use the initials bar.
536
     */
537
    public function query_db($pagesize, $useinitialsbar = true) {
538
        global $DB;
539
 
540
        list($countsql, $countparams) = $this->get_sql_and_params(true);
541
        list($sql, $params) = $this->get_sql_and_params();
542
        $total = $DB->count_records_sql($countsql, $countparams);
543
        $this->pagesize($pagesize, $total);
544
        if ($this->is_downloading()) {
545
            $histories = $DB->get_records_sql($sql, $params);
546
        } else {
547
            $histories = $DB->get_records_sql($sql, $params, $this->pagesize * $this->currpage, $this->pagesize);
548
        }
549
        foreach ($histories as $history) {
550
            $this->rawdata[] = $history;
551
        }
552
        // Set initial bars.
553
        if ($useinitialsbar) {
554
            $this->initialbars($total > $pagesize);
555
        }
556
    }
557
 
558
    /**
559
     * Returns a list of selected users.
560
     *
561
     * @return \stdClass[] List of user objects
562
     */
563
    public function get_selected_users(): array {
564
        return $this->users;
565
    }
566
}