Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]

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