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
 * Contains class mod_feedback_responses_table
19
 *
20
 * @package   mod_feedback
21
 * @copyright 2016 Marina Glancy
22
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 */
24
 
25
defined('MOODLE_INTERNAL') || die();
26
 
27
global $CFG;
28
require_once($CFG->libdir . '/tablelib.php');
29
 
30
/**
31
 * Class mod_feedback_responses_table
32
 *
33
 * @package   mod_feedback
34
 * @copyright 2016 Marina Glancy
35
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
36
 */
37
class mod_feedback_responses_table extends table_sql {
38
 
39
    /**
40
     * Maximum number of feedback questions to display in the "Show responses" table
41
     */
42
    const PREVIEWCOLUMNSLIMIT = 10;
43
 
44
    /**
45
     * Maximum number of feedback questions answers to retrieve in one SQL query.
46
     * Mysql has a limit of 60, we leave 1 for joining with users table.
47
     */
48
    const TABLEJOINLIMIT = 59;
49
 
50
    /**
51
     * When additional queries are needed to retrieve more than TABLEJOINLIMIT questions answers, do it in chunks every x rows.
52
     * Value too small will mean too many DB queries, value too big may cause memory overflow.
53
     */
54
    const ROWCHUNKSIZE = 100;
55
 
56
    /** @var mod_feedback_structure */
57
    protected $feedbackstructure;
58
 
59
    /** @var int */
60
    protected $grandtotal = null;
61
 
62
    /** @var bool */
63
    protected $showall = false;
64
 
65
    /** @var string */
66
    protected $showallparamname = 'showall';
67
 
68
    /** @var string */
69
    protected $downloadparamname = 'download';
70
 
71
    /** @var int number of columns that were not retrieved in the main SQL query
72
     * (no more than TABLEJOINLIMIT tables with values can be joined). */
73
    protected $hasmorecolumns = 0;
74
 
75
    /** @var bool whether we are building this table for a external function */
76
    protected $buildforexternal = false;
77
 
78
    /** @var array the data structure containing the table data for the external function */
79
    protected $dataforexternal = [];
80
 
81
    /** @var bool true if elements per page > 0, otherwise false. */
82
    protected $pageable;
83
 
84
    /**
85
     * Constructor
86
     *
87
     * @param mod_feedback_structure $feedbackstructure
88
     * @param int $group retrieve only users from this group (optional)
89
     */
90
    public function __construct(mod_feedback_structure $feedbackstructure, $group = 0) {
91
        $this->feedbackstructure = $feedbackstructure;
92
 
93
        parent::__construct('feedback-showentry-list-' . $feedbackstructure->get_cm()->instance);
94
 
95
        $this->showall = optional_param($this->showallparamname, 0, PARAM_BOOL);
96
        $this->define_baseurl(new moodle_url('/mod/feedback/show_entries.php',
97
            ['id' => $this->feedbackstructure->get_cm()->id]));
98
        if ($courseid = $this->feedbackstructure->get_courseid()) {
99
            $this->baseurl->param('courseid', $courseid);
100
        }
101
        if ($this->showall) {
102
            $this->baseurl->param($this->showallparamname, $this->showall);
103
        }
104
 
105
        $name = format_string($feedbackstructure->get_feedback()->name);
106
        $this->is_downloadable(true);
107
        $this->is_downloading(optional_param($this->downloadparamname, 0, PARAM_ALPHA),
108
                $name, get_string('responses', 'feedback'));
109
        $this->useridfield = 'userid';
110
        $this->init($group);
111
    }
112
 
113
    /**
114
     * Initialises table
115
     * @param int $group retrieve only users from this group (optional)
116
     */
117
    protected function init($group = 0) {
118
 
119
        $tablecolumns = array('userpic', 'fullname', 'groups');
120
        $tableheaders = array(
121
            get_string('userpic'),
122
            get_string('fullnameuser'),
123
            get_string('groups')
124
        );
125
 
126
        // TODO Does not support custom user profile fields (MDL-70456).
127
        $userfieldsapi = \core_user\fields::for_identity($this->get_context(), false)->with_userpic();
128
        $ufields = $userfieldsapi->get_sql('u', false, '', $this->useridfield, false)->selects;
129
        $extrafields = $userfieldsapi->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]);
130
        $fields = 'c.id, c.timemodified as completed_timemodified, c.courseid, '.$ufields;
131
        $from = '{feedback_completed} c '
132
                . 'JOIN {user} u ON u.id = c.userid AND u.deleted = :notdeleted';
133
        $where = 'c.anonymous_response = :anon
134
                AND c.feedback = :instance';
135
        if ($this->feedbackstructure->get_courseid()) {
136
            $where .= ' AND c.courseid = :courseid';
137
        }
138
 
139
        if ($this->is_downloading()) {
140
            // When downloading data:
141
            // Remove 'userpic' from downloaded data.
142
            array_shift($tablecolumns);
143
            array_shift($tableheaders);
144
 
145
            // Add all identity fields as separate columns.
146
            foreach ($extrafields as $field) {
147
                $fields .= ", u.{$field}";
148
                $tablecolumns[] = $field;
149
                $tableheaders[] = \core_user\fields::get_display_name($field);
150
            }
151
        }
152
 
153
        if ($this->feedbackstructure->get_feedback()->course == SITEID && !$this->feedbackstructure->get_courseid()) {
154
            $tablecolumns[] = 'courseid';
155
            $tableheaders[] = get_string('course');
156
        }
157
 
158
        $tablecolumns[] = 'completed_timemodified';
159
        $tableheaders[] = get_string('date');
160
 
161
        $this->define_columns($tablecolumns);
162
        $this->define_headers($tableheaders);
163
 
164
        $this->sortable(true, 'lastname', SORT_ASC);
165
        $this->no_sorting('groups');
166
        $this->collapsible(true);
167
        $this->set_attribute('id', 'showentrytable');
168
 
169
        $params = array();
170
        $params['anon'] = FEEDBACK_ANONYMOUS_NO;
171
        $params['instance'] = $this->feedbackstructure->get_feedback()->id;
172
        $params['notdeleted'] = 0;
173
        $params['courseid'] = $this->feedbackstructure->get_courseid();
174
 
175
        $group = (empty($group)) ? groups_get_activity_group($this->feedbackstructure->get_cm(), true) : $group;
176
        if ($group) {
177
            $where .= ' AND c.userid IN (SELECT g.userid FROM {groups_members} g WHERE g.groupid = :group)';
178
            $params['group'] = $group;
179
        }
180
 
181
        $this->set_sql($fields, $from, $where, $params);
182
        $this->set_count_sql("SELECT COUNT(c.id) FROM $from WHERE $where", $params);
183
    }
184
 
185
    /**
186
     * Current context
187
     * @return context_module
188
     */
189
    public function get_context(): context {
190
        return context_module::instance($this->feedbackstructure->get_cm()->id);
191
    }
192
 
193
    /**
194
     * Allows to set the display column value for all columns without "col_xxxxx" method.
195
     * @param string $column column name
196
     * @param stdClass $row current record result of SQL query
197
     */
198
    public function other_cols($column, $row) {
199
        if (preg_match('/^val(\d+)$/', $column, $matches)) {
200
            $items = $this->feedbackstructure->get_items();
201
            $itemobj = feedback_get_item_class($items[$matches[1]]->typ);
202
            $printval = $itemobj->get_printval($items[$matches[1]], (object) ['value' => $row->$column]);
203
            if ($this->is_downloading()) {
204
                $printval = s($printval);
205
            }
206
            return trim($printval);
207
        }
208
        return parent::other_cols($column, $row);
209
    }
210
 
211
    /**
212
     * Prepares column userpic for display
213
     * @param stdClass $row
214
     * @return string
215
     */
216
    public function col_userpic($row) {
217
        global $OUTPUT;
218
        $user = user_picture::unalias($row, [], $this->useridfield);
219
        return $OUTPUT->user_picture($user, array('courseid' => $this->feedbackstructure->get_cm()->course));
220
    }
221
 
222
    /**
223
     * Prepares column deleteentry for display
224
     * @param stdClass $row
225
     * @return string
226
     */
227
    public function col_deleteentry($row) {
228
        global $OUTPUT;
229
        $deleteentryurl = new moodle_url($this->baseurl, ['delete' => $row->id, 'sesskey' => sesskey()]);
230
        $deleteaction = new confirm_action(get_string('confirmdeleteentry', 'feedback'));
231
        return $OUTPUT->action_icon($deleteentryurl,
232
            new pix_icon('t/delete', get_string('delete_entry', 'feedback')), $deleteaction);
233
    }
234
 
235
    /**
236
     * Returns a link for viewing a single response
237
     * @param stdClass $row
238
     * @return \moodle_url
239
     */
240
    protected function get_link_single_entry($row) {
241
        return new moodle_url($this->baseurl, ['userid' => $row->{$this->useridfield}, 'showcompleted' => $row->id]);
242
    }
243
 
244
    /**
245
     * Prepares column completed_timemodified for display
246
     * @param stdClass $student
247
     * @return string
248
     */
249
    public function col_completed_timemodified($student) {
250
        if ($this->is_downloading()) {
251
            return userdate($student->completed_timemodified);
252
        } else {
253
            return html_writer::link($this->get_link_single_entry($student),
254
                    userdate($student->completed_timemodified));
255
        }
256
    }
257
 
258
    /**
259
     * Prepares column courseid for display
260
     * @param array $row
261
     * @return string
262
     */
263
    public function col_courseid($row) {
264
        $courses = $this->feedbackstructure->get_completed_courses();
265
        $name = '';
266
        if (isset($courses[$row->courseid])) {
267
            $name = $courses[$row->courseid];
268
            if (!$this->is_downloading()) {
269
                $name = html_writer::link(course_get_url($row->courseid), $name);
270
            }
271
        }
272
        return $name;
273
    }
274
 
275
    /**
276
     * Prepares column groups for display
277
     * @param array $row
278
     * @return string
279
     */
280
    public function col_groups($row) {
281
        $groups = '';
282
        if ($usergrps = groups_get_all_groups($this->feedbackstructure->get_cm()->course, $row->userid, 0, 'name')) {
283
            foreach ($usergrps as $group) {
284
                $groups .= format_string($group->name). ' ';
285
            }
286
        }
287
        return trim($groups);
288
    }
289
 
290
    /**
291
     * Adds common values to the table that do not change the number or order of entries and
292
     * are only needed when outputting or downloading data.
293
     */
294
    protected function add_all_values_to_output() {
295
        global $DB;
296
 
297
        $tablecolumns = array_keys($this->columns);
298
        $tableheaders = $this->headers;
299
 
300
        $items = $this->feedbackstructure->get_items(true);
301
        if (!$this->is_downloading() && !$this->buildforexternal) {
302
            // In preview mode do not show all columns or the page becomes unreadable.
303
            // The information message will be displayed to the teacher that the rest of the data can be viewed when downloading.
304
            $items = array_slice($items, 0, self::PREVIEWCOLUMNSLIMIT, true);
305
        }
306
 
307
        $columnscount = 0;
308
        $this->hasmorecolumns = max(0, count($items) - self::TABLEJOINLIMIT);
309
 
310
        $headernamepostfix = !$this->is_downloading();
311
        // Add feedback response values.
312
        foreach ($items as $nr => $item) {
313
            if ($columnscount++ < self::TABLEJOINLIMIT) {
314
                // Mysql has a limit on the number of tables in the join, so we only add limited number of columns here,
315
                // the rest will be added in {@link self::build_table()} and {@link self::build_table_chunk()} functions.
316
                $this->sql->fields .= ", " . $DB->sql_cast_to_char("v{$nr}.value") . " AS val{$nr}";
317
                $this->sql->from .= " LEFT OUTER JOIN {feedback_value} v{$nr} " .
318
                    "ON v{$nr}.completed = c.id AND v{$nr}.item = :itemid{$nr}";
319
                $this->sql->params["itemid{$nr}"] = $item->id;
320
            }
321
 
322
            $tablecolumns[] = "val{$nr}";
323
            $itemobj = feedback_get_item_class($item->typ);
324
            $columnheader = $itemobj->get_display_name($item, $headernamepostfix);
325
            if (!$this->is_downloading()) {
326
                $columnheader = shorten_text($columnheader);
327
            }
328
            if (strval($item->label) !== '') {
329
                $columnheader = get_string('nameandlabelformat', 'mod_feedback',
330
                    (object)['label' => format_string($item->label), 'name' => $columnheader]);
331
            }
332
            $tableheaders[] = $columnheader;
333
        }
334
 
335
        // Add 'Delete entry' column.
336
        if (!$this->is_downloading() && has_capability('mod/feedback:deletesubmissions', $this->get_context())) {
337
            $tablecolumns[] = 'deleteentry';
338
            $tableheaders[] = '';
339
        }
340
 
341
        $this->define_columns($tablecolumns);
342
        $this->define_headers($tableheaders);
343
    }
344
 
345
    /**
346
     * Query the db. Store results in the table object for use by build_table.
347
     *
348
     * @param int $pagesize size of page for paginated displayed table.
349
     * @param bool $useinitialsbar do you want to use the initials bar. Bar
350
     * will only be used if there is a fullname column defined for the table.
351
     */
352
    public function query_db($pagesize, $useinitialsbar=true) {
353
        global $DB;
354
        $this->totalrows = $grandtotal = $this->get_total_responses_count();
355
        if (!$this->is_downloading()) {
356
            $this->initialbars($useinitialsbar);
357
 
358
            list($wsql, $wparams) = $this->get_sql_where();
359
            if ($wsql) {
360
                $this->countsql .= ' AND '.$wsql;
361
                $this->countparams = array_merge($this->countparams, $wparams);
362
 
363
                $this->sql->where .= ' AND '.$wsql;
364
                $this->sql->params = array_merge($this->sql->params, $wparams);
365
 
366
                $this->totalrows  = $DB->count_records_sql($this->countsql, $this->countparams);
367
            }
368
 
369
            if ($this->totalrows > $pagesize) {
370
                $this->pagesize($pagesize, $this->totalrows);
371
            }
372
        }
373
 
374
        if ($sort = $this->get_sql_sort()) {
375
            $sort = "ORDER BY $sort";
376
        }
377
        $sql = "SELECT
378
                {$this->sql->fields}
379
                FROM {$this->sql->from}
380
                WHERE {$this->sql->where}
381
                {$sort}";
382
 
383
        if (!$this->is_downloading()) {
384
            $this->rawdata = $DB->get_recordset_sql($sql, $this->sql->params, $this->get_page_start(), $this->get_page_size());
385
        } else {
386
            $this->rawdata = $DB->get_recordset_sql($sql, $this->sql->params);
387
        }
388
    }
389
 
390
    /**
391
     * Returns total number of reponses (without any filters applied)
392
     * @return int
393
     */
394
    public function get_total_responses_count() {
395
        global $DB;
396
        if ($this->grandtotal === null) {
397
            $this->grandtotal = $DB->count_records_sql($this->countsql, $this->countparams);
398
        }
399
        return $this->grandtotal;
400
    }
401
 
402
    /**
403
     * Defines columns
404
     * @param array $columns an array of identifying names for columns. If
405
     * columns are sorted then column names must correspond to a field in sql.
406
     */
407
    public function define_columns($columns) {
408
        parent::define_columns($columns);
409
        foreach ($this->columns as $column => $column) {
410
            // Automatically assign classes to columns.
411
            $this->column_class[$column] = ' ' . $column;
412
        }
413
    }
414
 
415
    /**
416
     * Convenience method to call a number of methods for you to display the
417
     * table.
418
     * @param int $pagesize
419
     * @param bool $useinitialsbar
420
     * @param string $downloadhelpbutton
421
     */
422
    public function out($pagesize, $useinitialsbar, $downloadhelpbutton='') {
423
        $this->add_all_values_to_output();
424
        parent::out($pagesize, $useinitialsbar, $downloadhelpbutton);
425
    }
426
 
427
    /**
428
     * Displays the table
429
     */
430
    public function display() {
431
        global $OUTPUT;
432
        groups_print_activity_menu($this->feedbackstructure->get_cm(), $this->baseurl->out());
433
        $grandtotal = $this->get_total_responses_count();
434
        if (!$grandtotal) {
435
            echo $OUTPUT->notification(get_string('nothingtodisplay'), 'info', false);
436
            return;
437
        }
438
 
439
        if (count($this->feedbackstructure->get_items(true)) > self::PREVIEWCOLUMNSLIMIT) {
440
            echo $OUTPUT->notification(get_string('questionslimited', 'feedback', self::PREVIEWCOLUMNSLIMIT), 'info');
441
        }
442
 
443
        $this->out($this->showall ? $grandtotal : FEEDBACK_DEFAULT_PAGE_COUNT,
444
                $grandtotal > FEEDBACK_DEFAULT_PAGE_COUNT);
445
 
446
        // Toggle 'Show all' link.
447
        if ($this->totalrows > FEEDBACK_DEFAULT_PAGE_COUNT) {
448
            if (!$this->use_pages) {
449
                echo html_writer::div(html_writer::link(new moodle_url($this->baseurl, [$this->showallparamname => 0]),
450
                        get_string('showperpage', '', FEEDBACK_DEFAULT_PAGE_COUNT)), 'showall');
451
            } else {
452
                echo html_writer::div(html_writer::link(new moodle_url($this->baseurl, [$this->showallparamname => 1]),
453
                        get_string('showall', '', $this->totalrows)), 'showall');
454
            }
455
        }
456
    }
457
 
458
    /**
459
     * Returns links to previous/next responses in the list
460
     * @param stdClass $record
461
     * @return array array of three elements [$prevresponseurl, $returnurl, $nextresponseurl]
462
     */
463
    public function get_reponse_navigation_links($record) {
464
        $this->setup();
465
        $grandtotal = $this->get_total_responses_count();
466
        $this->query_db($grandtotal);
467
        $lastrow = $thisrow = $nextrow = null;
468
        $counter = 0;
469
        $page = 0;
470
        while ($this->rawdata->valid()) {
471
            $row = $this->rawdata->current();
472
            if ($row->id == $record->id) {
473
                $page = $this->showall ? 0 : floor($counter / FEEDBACK_DEFAULT_PAGE_COUNT);
474
                $thisrow = $row;
475
                $this->rawdata->next();
476
                $nextrow = $this->rawdata->valid() ? $this->rawdata->current() : null;
477
                break;
478
            }
479
            $lastrow = $row;
480
            $this->rawdata->next();
481
            $counter++;
482
        }
483
        $this->rawdata->close();
484
        if (!$thisrow) {
485
            $lastrow = null;
486
        }
487
        return [
488
            $lastrow ? $this->get_link_single_entry($lastrow) : null,
489
            new moodle_url($this->baseurl, [$this->request[TABLE_VAR_PAGE] => $page]),
490
            $nextrow ? $this->get_link_single_entry($nextrow) : null,
491
        ];
492
    }
493
 
494
    /**
495
     * Download the data.
496
     */
497
    public function download() {
498
        \core\session\manager::write_close();
499
        $this->out($this->get_total_responses_count(), false);
500
        exit;
501
    }
502
 
503
    /**
504
     * Take the data returned from the db_query and go through all the rows
505
     * processing each col using either col_{columnname} method or other_cols
506
     * method or if other_cols returns NULL then put the data straight into the
507
     * table.
508
     *
509
     * This overwrites the parent method because full SQL query may fail on Mysql
510
     * because of the limit in the number of tables in the join. Therefore we only
511
     * join 59 tables in the main query and add the rest here.
512
     *
513
     * @return void
514
     */
515
    public function build_table() {
516
        if ($this->rawdata instanceof \Traversable && !$this->rawdata->valid()) {
517
            return;
518
        }
519
        if (!$this->rawdata) {
520
            return;
521
        }
522
 
523
        $columnsgroups = [];
524
        if ($this->hasmorecolumns) {
525
            $items = $this->feedbackstructure->get_items(true);
526
            $notretrieveditems = array_slice($items, self::TABLEJOINLIMIT, $this->hasmorecolumns, true);
527
            $columnsgroups = array_chunk($notretrieveditems, self::TABLEJOINLIMIT, true);
528
        }
529
 
530
        $chunk = [];
531
        foreach ($this->rawdata as $row) {
532
            if ($this->hasmorecolumns) {
533
                $chunk[$row->id] = $row;
534
                if (count($chunk) >= self::ROWCHUNKSIZE) {
535
                    $this->build_table_chunk($chunk, $columnsgroups);
536
                    $chunk = [];
537
                }
538
            } else {
539
                if ($this->buildforexternal) {
540
                    $this->add_data_for_external($row);
541
                } else {
542
                    $this->add_data_keyed($this->format_row($row), $this->get_row_class($row));
543
                }
544
            }
545
        }
546
        $this->build_table_chunk($chunk, $columnsgroups);
547
    }
548
 
549
    /**
550
     * Retrieve additional columns. Database engine may have a limit on number of joins.
551
     *
552
     * @param array $rows Array of rows with already retrieved data, new values will be added to this array
553
     * @param array $columnsgroups array of arrays of columns. Each element has up to self::TABLEJOINLIMIT items. This
554
     *     is easy to calculate but because we can call this method many times we calculate it once and pass by
555
     *     reference for performance reasons
556
     */
557
    protected function build_table_chunk(&$rows, &$columnsgroups) {
558
        global $DB;
559
        if (!$rows) {
560
            return;
561
        }
562
 
563
        foreach ($columnsgroups as $columnsgroup) {
564
            $fields = 'c.id';
565
            $from = '{feedback_completed} c';
566
            $params = [];
567
            foreach ($columnsgroup as $nr => $item) {
568
                $fields .= ", " . $DB->sql_cast_to_char("v{$nr}.value") . " AS val{$nr}";
569
                $from .= " LEFT OUTER JOIN {feedback_value} v{$nr} " .
570
                    "ON v{$nr}.completed = c.id AND v{$nr}.item = :itemid{$nr}";
571
                $params["itemid{$nr}"] = $item->id;
572
            }
573
            list($idsql, $idparams) = $DB->get_in_or_equal(array_keys($rows), SQL_PARAMS_NAMED);
574
            $sql = "SELECT $fields FROM $from WHERE c.id ".$idsql;
575
            $results = $DB->get_records_sql($sql, $params + $idparams);
576
            foreach ($results as $result) {
577
                foreach ($result as $key => $value) {
578
                    $rows[$result->id]->{$key} = $value;
579
                }
580
            }
581
        }
582
 
583
        foreach ($rows as $row) {
584
            if ($this->buildforexternal) {
585
                $this->add_data_for_external($row);
586
            } else {
587
                $this->add_data_keyed($this->format_row($row), $this->get_row_class($row));
588
            }
589
        }
590
    }
591
 
592
    /**
593
     * Returns html code for displaying "Download" button if applicable.
594
     */
595
    public function download_buttons() {
596
        global $OUTPUT;
597
 
598
        if ($this->is_downloadable() && !$this->is_downloading()) {
599
            return $OUTPUT->download_dataformat_selector(get_string('downloadas', 'table'),
600
                    $this->baseurl->out_omit_querystring(), $this->downloadparamname, $this->baseurl->params());
601
        } else {
602
            return '';
603
        }
604
    }
605
 
606
    /**
607
     * Return user responses data ready for the external function.
608
     *
609
     * @param stdClass $row the table row containing the responses
610
     * @return array returns the responses ready to be used by an external function
611
     * @since Moodle 3.3
612
     */
613
    protected function get_responses_for_external($row) {
614
        $responses = [];
615
        foreach ($row as $el => $val) {
616
            // Get id from column name.
617
            if (preg_match('/^val(\d+)$/', $el, $matches)) {
618
                $id = $matches[1];
619
 
620
                $responses[] = [
621
                    'id' => $id,
622
                    'name' => $this->headers[$this->columns[$el]],
623
                    'printval' => $this->other_cols($el, $row),
624
                    'rawval' => $val,
625
                ];
626
            }
627
        }
628
        return $responses;
629
    }
630
 
631
    /**
632
     * Add data for the external structure that will be returned.
633
     *
634
     * @param stdClass $row a database query record row
635
     * @since Moodle 3.3
636
     */
637
    protected function add_data_for_external($row) {
638
        $this->dataforexternal[] = [
639
            'id' => $row->id,
640
            'courseid' => $row->courseid,
641
            'userid' => $row->userid,
642
            'fullname' => fullname($row),
643
            'timemodified' => $row->completed_timemodified,
644
            'responses' => $this->get_responses_for_external($row),
645
        ];
646
    }
647
 
648
    /**
649
     * Exports the table as an external structure handling pagination.
650
     *
651
     * @param int $page page number (for pagination)
652
     * @param int $perpage elements per page
653
     * @since Moodle 3.3
654
     * @return array returns the table ready to be used by an external function
655
     */
656
    public function export_external_structure($page = 0, $perpage = 0) {
657
 
658
        $this->buildforexternal = true;
659
        $this->add_all_values_to_output();
660
        // Set-up.
661
        $this->setup();
662
        // Override values, if needed.
663
        if ($perpage > 0) {
664
            $this->pageable = true;
665
            $this->currpage = $page;
666
            $this->pagesize = $perpage;
667
        } else {
668
            $this->pagesize = $this->get_total_responses_count();
669
        }
670
        $this->query_db($this->pagesize, false);
671
        $this->build_table();
672
        $this->close_recordset();
673
        return $this->dataforexternal;
674
    }
675
}