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
/**
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
    /**
1441 ariadna 217
     * Display name of country
218
     *
219
     * @param \stdClass $history
220
     * @return string
221
     */
222
    public function col_country(\stdClass $history): string {
223
        $countries = get_string_manager()->get_list_of_countries();
224
        return $countries[$history->country] ?? $history->country;
225
    }
226
 
227
    /**
1 efrain 228
     * Method to display the final grade.
229
     *
230
     * @param \stdClass $history an entry of history record.
231
     *
232
     * @return string HTML to display
233
     */
234
    public function col_finalgrade(\stdClass $history) {
235
        if (!empty($this->gradeitems[$history->itemid])) {
236
            $decimalpoints = $this->gradeitems[$history->itemid]->get_decimals();
237
        } else {
238
            $decimalpoints = $this->defaultdecimalpoints;
239
        }
240
 
241
        return format_float($history->finalgrade, $decimalpoints);
242
    }
243
 
244
    /**
245
     * Method to display the previous grade.
246
     *
247
     * @param \stdClass $history an entry of history record.
248
     *
249
     * @return string HTML to display
250
     */
251
    public function col_prevgrade(\stdClass $history) {
252
        if (!empty($this->gradeitems[$history->itemid])) {
253
            $decimalpoints = $this->gradeitems[$history->itemid]->get_decimals();
254
        } else {
255
            $decimalpoints = $this->defaultdecimalpoints;
256
        }
257
 
258
        return format_float($history->prevgrade, $decimalpoints);
259
    }
260
 
261
    /**
262
     * Method to display column timemodifed.
263
     *
264
     * @param \stdClass $history an entry of history record.
265
     *
266
     * @return string HTML to display
267
     */
268
    public function col_timemodified(\stdClass $history) {
269
        return userdate($history->timemodified);
270
    }
271
 
272
    /**
273
     * Method to display column itemname.
274
     *
275
     * @param \stdClass $history an entry of history record.
276
     *
277
     * @return string HTML to display
278
     */
279
    public function col_itemname(\stdClass $history) {
280
        // Make sure grade item is still present and link it to the module if possible.
281
        $itemid = $history->itemid;
282
        if (!empty($this->gradeitems[$itemid])) {
283
            if ($history->itemtype === 'mod' && !$this->is_downloading()) {
284
                if (!empty($this->cms->instances[$history->itemmodule][$history->iteminstance])) {
285
                    $cm = $this->cms->instances[$history->itemmodule][$history->iteminstance];
286
                    $url = new \moodle_url('/mod/' . $history->itemmodule . '/view.php', array('id' => $cm->id));
287
                    return \html_writer::link($url, $this->gradeitems[$itemid]->get_name());
288
                }
289
            }
290
            return $this->gradeitems[$itemid]->get_name();
291
        }
292
        return get_string('deleteditemid', 'gradereport_history', $history->itemid);
293
    }
294
 
295
    /**
296
     * Method to display column grader.
297
     *
298
     * @param \stdClass $history an entry of history record.
299
     *
300
     * @return string HTML to display
301
     */
302
    public function col_grader(\stdClass $history) {
303
        if (empty($history->usermodified)) {
304
            // Not every row has a valid usermodified.
305
            return '';
306
        }
307
 
308
        $grader = new \stdClass();
309
        $grader = username_load_fields_from_object($grader, $history, 'grader');
310
        $name = fullname($grader);
311
 
312
        if ($this->download) {
313
            return $name;
314
        }
315
 
316
        $userid = $history->usermodified;
317
        $profileurl = new \moodle_url('/user/view.php', array('id' => $userid, 'course' => $this->courseid));
318
 
319
        return \html_writer::link($profileurl, $name);
320
    }
321
 
322
    /**
323
     * Method to display column overridden.
324
     *
325
     * @param \stdClass $history an entry of history record.
326
     *
327
     * @return string HTML to display
328
     */
329
    public function col_overridden(\stdClass $history) {
330
        return $history->overridden ? get_string('yes') : get_string('no');
331
    }
332
 
333
    /**
334
     * Method to display column locked.
335
     *
336
     * @param \stdClass $history an entry of history record.
337
     *
338
     * @return string HTML to display
339
     */
340
    public function col_locked(\stdClass $history) {
341
        return $history->locked ? get_string('yes') : get_string('no');
342
    }
343
 
344
    /**
345
     * Method to display column excluded.
346
     *
347
     * @param \stdClass $history an entry of history record.
348
     *
349
     * @return string HTML to display
350
     */
351
    public function col_excluded(\stdClass $history) {
352
        return $history->excluded ? get_string('yes') : get_string('no');
353
    }
354
 
355
    /**
356
     * Method to display column feedback.
357
     *
358
     * @param \stdClass $history an entry of history record.
359
     *
360
     * @return string HTML to display
361
     */
362
    public function col_feedback(\stdClass $history) {
363
        if ($this->is_downloading()) {
364
            return $history->feedback;
365
        } else {
366
            // We need the activity context, not the course context.
367
            $gradeitem = $this->gradeitems[$history->itemid];
368
            $context = $gradeitem->get_context();
369
 
370
            $feedback = file_rewrite_pluginfile_urls(
371
                $history->feedback,
372
                'pluginfile.php',
373
                $context->id,
374
                GRADE_FILE_COMPONENT,
375
                GRADE_HISTORY_FEEDBACK_FILEAREA,
376
                $history->id
377
            );
378
 
379
            return format_text($feedback, $history->feedbackformat, array('context' => $context));
380
        }
381
    }
382
 
383
    /**
384
     * Builds the sql and param list needed, based on the user selected filters.
385
     *
386
     * @return array containing sql to use and an array of params.
387
     */
388
    protected function get_filters_sql_and_params() {
389
        global $DB, $USER;
390
 
391
        $coursecontext = $this->context;
392
        $filter = 'gi.courseid = :courseid';
393
        $params = array(
394
            'courseid' => $coursecontext->instanceid,
395
        );
396
 
397
        if (!empty($this->filters->itemid)) {
398
            $filter .= ' AND ggh.itemid = :itemid';
399
            $params['itemid'] = $this->filters->itemid;
400
        }
401
        if (!empty($this->filters->userids)) {
402
            $list = explode(',', $this->filters->userids);
403
            list($insql, $plist) = $DB->get_in_or_equal($list, SQL_PARAMS_NAMED);
404
            $filter .= " AND ggh.userid $insql";
405
            $params += $plist;
406
        }
407
        if (!empty($this->filters->datefrom)) {
408
            $filter .= " AND ggh.timemodified >= :datefrom";
409
            $params += array('datefrom' => $this->filters->datefrom);
410
        }
411
        if (!empty($this->filters->datetill)) {
412
            $filter .= " AND ggh.timemodified <= :datetill";
413
            $params += array('datetill' => $this->filters->datetill);
414
        }
415
        if (!empty($this->filters->grader)) {
416
            $filter .= " AND ggh.usermodified = :grader";
417
            $params += array('grader' => $this->filters->grader);
418
        }
419
 
420
        // If the course is separate group mode and the current user is not allowed to see all groups make sure
421
        // that we display only users from the same groups as current user.
422
        $groupmode = get_course($coursecontext->instanceid)->groupmode;
423
        if ($groupmode == SEPARATEGROUPS && !has_capability('moodle/site:accessallgroups', $coursecontext)) {
424
            $groupids = array_column(groups_get_all_groups($coursecontext->instanceid, $USER->id, 0, 'g.id'), 'id');
425
            list($gsql, $gparams) = $DB->get_in_or_equal($groupids, SQL_PARAMS_NAMED, 'gmuparam', true, 0);
426
            $filter .= " AND EXISTS (SELECT 1 FROM {groups_members} gmu WHERE gmu.userid=ggh.userid AND gmu.groupid $gsql)";
427
            $params += $gparams;
428
        }
429
 
430
        return array($filter, $params);
431
    }
432
 
433
    /**
434
     * Builds the complete sql with all the joins to get the grade history data.
435
     *
436
     * @param bool $count setting this to true, returns an sql to get count only instead of the complete data records.
437
     *
438
     * @return array containing sql to use and an array of params.
439
     */
440
    protected function get_sql_and_params($count = false) {
441
        $fields = 'ggh.id, ggh.timemodified, ggh.itemid, ggh.userid, ggh.finalgrade, ggh.usermodified,
442
                   ggh.source, ggh.overridden, ggh.locked, ggh.excluded, ggh.feedback, ggh.feedbackformat,
443
                   gi.itemtype, gi.itemmodule, gi.iteminstance, gi.itemnumber, ';
444
 
445
        $userfieldsapi = \core_user\fields::for_identity($this->context);
446
        $userfieldssql = $userfieldsapi->get_sql('u', true, '', '', true);
447
        $userfieldsselects = '';
448
        $userfieldsjoins = '';
449
        $userfieldsparams = [];
450
        if (!$count) {
451
            $userfieldsselects = $userfieldssql->selects;
452
            $userfieldsjoins = $userfieldssql->joins;
453
            $userfieldsparams = $userfieldssql->params;
454
        }
455
 
456
        // Add extra user fields that we need for the graded user.
457
        $extrafields = [];
458
        foreach ($userfieldsapi->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]) as $field) {
459
            $extrafields[$field] = $userfieldssql->mappings[$field];
460
        }
461
        $userfieldsapi = \core_user\fields::for_name();
462
        $fields .= $userfieldsapi->get_sql('u', false, '', '', false)->selects . ', ';
463
        $groupby = $fields;
464
 
465
        // Add extra user fields that we need for the grader user.
466
        $fields .= $userfieldsapi->get_sql('ug', false, 'grader', '', false)->selects;
467
        $groupby .= $userfieldsapi->get_sql('ug', false, '', '', false)->selects;
468
 
469
        // Filtering on revised grades only.
470
        $revisedonly = !empty($this->filters->revisedonly);
471
 
472
        if ($count && !$revisedonly) {
473
            // We can only directly use count when not using the filter revised only.
474
            $select = "COUNT(1)";
475
        } else {
476
            // Fetching the previous grade. We use MAX() to ensure that we only get one result if
477
            // more than one histories happened at the same second.
478
            $prevgrade = "SELECT MAX(finalgrade)
479
                            FROM {grade_grades_history} h
480
                           WHERE h.itemid = ggh.itemid
481
                             AND h.userid = ggh.userid
1441 ariadna 482
                             AND (
483
                                    h.timemodified < ggh.timemodified
484
                                    OR (h.timemodified = ggh.timemodified AND h.source != ggh.source AND h.id < ggh.id)
485
                                 )
1 efrain 486
                             AND NOT EXISTS (
487
                              SELECT 1
488
                                FROM {grade_grades_history} h2
489
                               WHERE h2.itemid = ggh.itemid
490
                                 AND h2.userid = ggh.userid
491
                                 AND h2.timemodified < ggh.timemodified
492
                                 AND h.timemodified < h2.timemodified)";
493
 
494
            $select = "$fields, ($prevgrade) AS prevgrade,
495
                      CASE WHEN gi.itemname IS NULL THEN gi.itemtype ELSE gi.itemname END AS itemname";
496
        }
497
 
498
        list($where, $params) = $this->get_filters_sql_and_params();
499
 
500
        $sql = " SELECT $select $userfieldsselects
501
                   FROM {grade_grades_history} ggh
502
                   JOIN {grade_items} gi ON gi.id = ggh.itemid
503
                   JOIN {user} u ON u.id = ggh.userid
504
                        $userfieldsjoins
505
              LEFT JOIN {user} ug ON ug.id = ggh.usermodified
506
                  WHERE $where";
507
        $params = array_merge($userfieldsparams, $params);
508
 
509
        // As prevgrade is a dynamic field, we need to wrap the query. This is the only filtering
510
        // that should be defined outside the method self::get_filters_sql_and_params().
511
        if ($revisedonly) {
512
            $allorcount = $count ? 'COUNT(1)' : '*';
513
            $sql = "SELECT $allorcount FROM ($sql) pg
514
                     WHERE pg.finalgrade != pg.prevgrade
515
                        OR (pg.prevgrade IS NULL AND pg.finalgrade IS NOT NULL)
516
                        OR (pg.prevgrade IS NOT NULL AND pg.finalgrade IS NULL)";
517
        }
518
 
519
        // Add order by if needed.
520
        if (!$count && $sqlsort = $this->get_sql_sort()) {
521
            $sql .= " ORDER BY " . $sqlsort;
522
        }
523
 
524
        return array($sql, $params);
525
    }
526
 
527
    /**
528
     * Get the SQL fragment to sort by.
529
     *
530
     * This is overridden to sort by timemodified and ID by default. Many items happen at the same time
531
     * and a second sorting by ID is valuable to distinguish the order in which the history happened.
532
     *
533
     * @return string SQL fragment.
534
     */
535
    public function get_sql_sort() {
536
        $columns = $this->get_sort_columns();
537
        if (count($columns) == 1 && isset($columns['timemodified']) && $columns['timemodified'] == SORT_DESC) {
538
            // Add the 'id' column when we are using the default sorting.
539
            $columns['id'] = SORT_DESC;
540
            return self::construct_order_by($columns);
541
        }
542
        return parent::get_sql_sort();
543
    }
544
 
545
    /**
546
     * Query the reader. Store results in the object for use by build_table.
547
     *
548
     * @param int $pagesize size of page for paginated displayed table.
549
     * @param bool $useinitialsbar do you want to use the initials bar.
550
     */
551
    public function query_db($pagesize, $useinitialsbar = true) {
552
        global $DB;
553
 
554
        list($countsql, $countparams) = $this->get_sql_and_params(true);
555
        list($sql, $params) = $this->get_sql_and_params();
556
        $total = $DB->count_records_sql($countsql, $countparams);
557
        $this->pagesize($pagesize, $total);
558
        if ($this->is_downloading()) {
559
            $histories = $DB->get_records_sql($sql, $params);
560
        } else {
561
            $histories = $DB->get_records_sql($sql, $params, $this->pagesize * $this->currpage, $this->pagesize);
562
        }
563
        foreach ($histories as $history) {
564
            $this->rawdata[] = $history;
565
        }
566
        // Set initial bars.
567
        if ($useinitialsbar) {
568
            $this->initialbars($total > $pagesize);
569
        }
570
    }
571
 
572
    /**
573
     * Returns a list of selected users.
574
     *
575
     * @return \stdClass[] List of user objects
576
     */
577
    public function get_selected_users(): array {
578
        return $this->users;
579
    }
580
}