Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403] [Versions 402 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   * View user acceptances to the policies
  19   *
  20   * @package     tool_policy
  21   * @copyright   2018 Marina Glancy
  22   * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  namespace tool_policy;
  26  
  27  use tool_policy\output\acceptances_filter;
  28  use tool_policy\output\renderer;
  29  use tool_policy\output\user_agreement;
  30  use core_user;
  31  use stdClass;
  32  
  33  defined('MOODLE_INTERNAL') || die();
  34  
  35  global $CFG;
  36  require_once($CFG->dirroot.'/lib/tablelib.php');
  37  
  38  /**
  39   * Class acceptances_table
  40   *
  41   * @package     tool_policy
  42   * @copyright   2018 Marina Glancy
  43   * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  44   */
  45  class acceptances_table extends \table_sql {
  46  
  47      /** @var array */
  48      protected $versionids;
  49  
  50      /** @var acceptances_filter */
  51      protected $acceptancesfilter;
  52  
  53      /** @var renderer */
  54      protected $output;
  55  
  56      /**
  57       * @var string[] The list of countries.
  58       */
  59      protected $countries;
  60  
  61      /** @var bool are there any users that this user can agree on behalf of */
  62      protected $canagreeany = false;
  63  
  64      /**
  65       * Constructor.
  66       *
  67       * @param string $uniqueid Table identifier.
  68       * @param acceptances_filter $acceptancesfilter
  69       * @param renderer $output
  70       */
  71      public function __construct($uniqueid, acceptances_filter $acceptancesfilter, renderer $output) {
  72          global $CFG;
  73          parent::__construct($uniqueid);
  74          $this->set_attribute('id', 'acceptancetable');
  75          $this->acceptancesfilter = $acceptancesfilter;
  76          $this->is_downloading(optional_param('download', 0, PARAM_ALPHA), 'user_acceptances');
  77          $this->baseurl = $acceptancesfilter->get_url();
  78          $this->output = $output;
  79  
  80          $this->versionids = [];
  81          $versions = $acceptancesfilter->get_versions();
  82          if (count($versions) > 1) {
  83              foreach ($versions as $version) {
  84                  $this->versionids[$version->id] = $version->name;
  85              }
  86          } else {
  87              $version = reset($versions);
  88              $this->versionids[$version->id] = $version->name;
  89              if ($version->status != policy_version::STATUS_ACTIVE) {
  90                  $this->versionids[$version->id] .= '<br>' . $version->revision;
  91              }
  92          }
  93  
  94          // TODO Does not support custom user profile fields (MDL-70456).
  95          $userfieldsapi = \core_user\fields::for_identity(\context_system::instance(), false)->with_userpic();
  96          $userfields = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
  97          $extrafields = $userfieldsapi->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]);
  98  
  99          $this->set_sql("$userfields",
 100              "{user} u",
 101              'u.id <> :siteguestid AND u.deleted = 0',
 102              ['siteguestid' => $CFG->siteguest]);
 103          if (!$this->is_downloading()) {
 104              $this->add_column_header('select', get_string('select'), false, 'colselect');
 105          }
 106          $this->add_column_header('fullname', get_string('fullnameuser', 'core'));
 107          foreach ($extrafields as $field) {
 108              $this->add_column_header($field, \core_user\fields::get_display_name($field));
 109          }
 110  
 111          if (!$this->is_downloading() && !has_capability('tool/policy:acceptbehalf', \context_system::instance())) {
 112              // We will need to check capability to accept on behalf in each user's context, preload users contexts.
 113              $this->sql->fields .= ',' . \context_helper::get_preload_record_columns_sql('ctx');
 114              $this->sql->from .= ' JOIN {context} ctx ON ctx.contextlevel = :usercontextlevel AND ctx.instanceid = u.id';
 115              $this->sql->params['usercontextlevel'] = CONTEXT_USER;
 116          }
 117  
 118          if ($this->acceptancesfilter->get_single_version()) {
 119              $this->configure_for_single_version();
 120          } else {
 121              $this->configure_for_multiple_versions();
 122          }
 123  
 124          $this->build_sql_for_search_string($extrafields);
 125          $this->build_sql_for_capability_filter();
 126          $this->build_sql_for_roles_filter();
 127  
 128          $this->sortable(true, 'firstname');
 129      }
 130  
 131      /**
 132       * Remove randomness from the list by always sorting by user id in the end
 133       *
 134       * @return array
 135       */
 136      public function get_sort_columns() {
 137          $c = parent::get_sort_columns();
 138          $c['u.id'] = SORT_ASC;
 139          return $c;
 140      }
 141  
 142      /**
 143       * Allows to add only one column name and header to the table (parent class methods only allow to set all).
 144       *
 145       * @param string $key
 146       * @param string $label
 147       * @param bool $sortable
 148       * @param string $columnclass
 149       */
 150      protected function add_column_header($key, $label, $sortable = true, $columnclass = '') {
 151          if (empty($this->columns)) {
 152              $this->define_columns([$key]);
 153              $this->define_headers([$label]);
 154          } else {
 155              $this->columns[$key] = count($this->columns);
 156              $this->column_style[$key] = array();
 157              $this->column_class[$key] = $columnclass;
 158              $this->columnsticky[$key] = '';
 159              $this->column_suppress[$key] = false;
 160              $this->headers[] = $label;
 161          }
 162          if ($columnclass !== null) {
 163              $this->column_class($key, $columnclass);
 164          }
 165          if (!$sortable) {
 166              $this->no_sorting($key);
 167          }
 168      }
 169  
 170      /**
 171       * Helper configuration method.
 172       */
 173      protected function configure_for_single_version() {
 174          $userfieldsapi = \core_user\fields::for_name();
 175          $userfieldsmod = $userfieldsapi->get_sql('m', false, 'mod', '', false)->selects;
 176          $v = key($this->versionids);
 177          $this->sql->fields .= ", $userfieldsmod, a{$v}.status AS status{$v}, a{$v}.note, ".
 178             "a{$v}.timemodified, a{$v}.usermodified AS usermodified{$v}";
 179  
 180          $join = "JOIN {tool_policy_acceptances} a{$v} ON a{$v}.userid = u.id AND a{$v}.policyversionid=:versionid{$v}";
 181          $filterstatus = $this->acceptancesfilter->get_status_filter();
 182          if ($filterstatus == 1) {
 183              $this->sql->from .= " $join AND a{$v}.status=1";
 184          } else if ($filterstatus == 2) {
 185              $this->sql->from .= " $join AND a{$v}.status=0";
 186          } else {
 187              $this->sql->from .= " LEFT $join";
 188          }
 189  
 190          $this->sql->from .= " LEFT JOIN {user} m ON m.id = a{$v}.usermodified AND m.id <> u.id AND a{$v}.status IS NOT NULL";
 191  
 192          $this->sql->params['versionid' . $v] = $v;
 193  
 194          if ($filterstatus === 0) {
 195              $this->sql->where .= " AND a{$v}.status IS NULL";
 196          }
 197  
 198          $this->add_column_header('status' . $v, get_string('response', 'tool_policy'));
 199          $this->add_column_header('timemodified', get_string('responseon', 'tool_policy'));
 200          $this->add_column_header('usermodified' . $v, get_string('responseby', 'tool_policy'));
 201          $this->add_column_header('note', get_string('acceptancenote', 'tool_policy'), false);
 202      }
 203  
 204      /**
 205       * Helper configuration method.
 206       */
 207      protected function configure_for_multiple_versions() {
 208          $this->add_column_header('statusall', get_string('acceptancestatusoverall', 'tool_policy'));
 209          $filterstatus = $this->acceptancesfilter->get_status_filter();
 210          $statusall = [];
 211          foreach ($this->versionids as $v => $versionname) {
 212              $this->sql->fields .= ", a{$v}.status AS status{$v}, a{$v}.usermodified AS usermodified{$v}";
 213              $join = "JOIN {tool_policy_acceptances} a{$v} ON a{$v}.userid = u.id AND a{$v}.policyversionid=:versionid{$v}";
 214              if ($filterstatus == 1) {
 215                  $this->sql->from .= " {$join} AND a{$v}.status=1";
 216              } else if ($filterstatus == 2) {
 217                  $this->sql->from .= " {$join} AND a{$v}.status=0";
 218              } else {
 219                  $this->sql->from .= " LEFT {$join}";
 220              }
 221              $this->sql->params['versionid' . $v] = $v;
 222              $this->add_column_header('status' . $v, $versionname);
 223              $statusall[] = "COALESCE(a{$v}.status, 0)";
 224          }
 225          $this->sql->fields .= ",".join('+', $statusall)." AS statusall";
 226  
 227          if ($filterstatus === 0) {
 228              $statussql = [];
 229              foreach ($this->versionids as $v => $versionname) {
 230                  $statussql[] = "a{$v}.status IS NULL";
 231              }
 232              $this->sql->where .= " AND (u.policyagreed = 0 OR ".join(" OR ", $statussql).")";
 233          }
 234      }
 235  
 236      /**
 237       * Download the data.
 238       */
 239      public function download() {
 240          \core\session\manager::write_close();
 241          $this->out(0, false);
 242          exit;
 243      }
 244  
 245      /**
 246       * Get sql to add to where statement.
 247       *
 248       * @return string
 249       */
 250      public function get_sql_where() {
 251          list($where, $params) = parent::get_sql_where();
 252          $where = preg_replace('/firstname/', 'u.firstname', $where);
 253          $where = preg_replace('/lastname/', 'u.lastname', $where);
 254          return [$where, $params];
 255      }
 256  
 257      /**
 258       * Helper SQL query builder.
 259       *
 260       * @param array $userfields
 261       */
 262      protected function build_sql_for_search_string($userfields) {
 263          global $DB, $USER;
 264  
 265          $search = $this->acceptancesfilter->get_search_strings();
 266          if (empty($search)) {
 267              return;
 268          }
 269  
 270          $wheres = [];
 271          $params = [];
 272          foreach ($search as $index => $keyword) {
 273              $searchkey1 = 'search' . $index . '1';
 274              $searchkey2 = 'search' . $index . '2';
 275              $searchkey3 = 'search' . $index . '3';
 276              $searchkey4 = 'search' . $index . '4';
 277              $searchkey5 = 'search' . $index . '5';
 278              $searchkey6 = 'search' . $index . '6';
 279              $searchkey7 = 'search' . $index . '7';
 280  
 281              $conditions = array();
 282              // Search by fullname.
 283              $fullname = $DB->sql_fullname('u.firstname', 'u.lastname');
 284              $conditions[] = $DB->sql_like($fullname, ':' . $searchkey1, false, false);
 285  
 286              // Search by email.
 287              $email = $DB->sql_like('u.email', ':' . $searchkey2, false, false);
 288              if (!in_array('email', $userfields)) {
 289                  $maildisplay = 'maildisplay' . $index;
 290                  $userid1 = 'userid' . $index . '1';
 291                  // Prevent users who hide their email address from being found by others
 292                  // who aren't allowed to see hidden email addresses.
 293                  $email = "(". $email ." AND (" .
 294                      "u.maildisplay <> :$maildisplay " .
 295                      "OR u.id = :$userid1". // User can always find himself.
 296                      "))";
 297                  $params[$maildisplay] = core_user::MAILDISPLAY_HIDE;
 298                  $params[$userid1] = $USER->id;
 299              }
 300              $conditions[] = $email;
 301  
 302              // Search by idnumber.
 303              $idnumber = $DB->sql_like('u.idnumber', ':' . $searchkey3, false, false);
 304              if (!in_array('idnumber', $userfields)) {
 305                  $userid2 = 'userid' . $index . '2';
 306                  // Users who aren't allowed to see idnumbers should at most find themselves
 307                  // when searching for an idnumber.
 308                  $idnumber = "(". $idnumber . " AND u.id = :$userid2)";
 309                  $params[$userid2] = $USER->id;
 310              }
 311              $conditions[] = $idnumber;
 312  
 313              // Search by middlename.
 314              $middlename = $DB->sql_like('u.middlename', ':' . $searchkey4, false, false);
 315              $conditions[] = $middlename;
 316  
 317              // Search by alternatename.
 318              $alternatename = $DB->sql_like('u.alternatename', ':' . $searchkey5, false, false);
 319              $conditions[] = $alternatename;
 320  
 321              // Search by firstnamephonetic.
 322              $firstnamephonetic = $DB->sql_like('u.firstnamephonetic', ':' . $searchkey6, false, false);
 323              $conditions[] = $firstnamephonetic;
 324  
 325              // Search by lastnamephonetic.
 326              $lastnamephonetic = $DB->sql_like('u.lastnamephonetic', ':' . $searchkey7, false, false);
 327              $conditions[] = $lastnamephonetic;
 328  
 329              $wheres[] = "(". implode(" OR ", $conditions) .") ";
 330              $params[$searchkey1] = "%$keyword%";
 331              $params[$searchkey2] = "%$keyword%";
 332              $params[$searchkey3] = "%$keyword%";
 333              $params[$searchkey4] = "%$keyword%";
 334              $params[$searchkey5] = "%$keyword%";
 335              $params[$searchkey6] = "%$keyword%";
 336              $params[$searchkey7] = "%$keyword%";
 337          }
 338  
 339          $this->sql->where .= ' AND '.join(' AND ', $wheres);
 340          $this->sql->params += $params;
 341      }
 342  
 343      /**
 344       * If there is a filter to find users who can/cannot accept on their own behalf add it to the SQL query
 345       */
 346      protected function build_sql_for_capability_filter() {
 347          global $CFG;
 348          $hascapability = $this->acceptancesfilter->get_capability_accept_filter();
 349          if ($hascapability === null) {
 350              return;
 351          }
 352  
 353          list($neededroles, $forbiddenroles) = get_roles_with_cap_in_context(\context_system::instance(), 'tool/policy:accept');
 354  
 355          if (empty($neededroles)) {
 356              // There are no roles that allow to accept agreement on one own's behalf.
 357              $this->sql->where .= $hascapability ? ' AND 1=0' : '';
 358              return;
 359          }
 360  
 361          if (empty($forbiddenroles)) {
 362              // There are no roles that prohibit to accept agreement on one own's behalf.
 363              $this->sql->where .= ' AND ' . $this->sql_has_role($neededroles, $hascapability);
 364              return;
 365          }
 366  
 367          $defaultuserroleid = isset($CFG->defaultuserroleid) ? $CFG->defaultuserroleid : 0;
 368          if (!empty($neededroles[$defaultuserroleid])) {
 369              // Default role allows to accept agreement. Make sure user has/does not have one of the roles prohibiting it.
 370              $this->sql->where .= ' AND ' . $this->sql_has_role($forbiddenroles, !$hascapability);
 371              return;
 372          }
 373  
 374          if ($hascapability) {
 375              // User has at least one role allowing to accept and no roles prohibiting.
 376              $this->sql->where .= ' AND ' . $this->sql_has_role($neededroles);
 377              $this->sql->where .= ' AND ' . $this->sql_has_role($forbiddenroles, false);
 378          } else {
 379              // Option 1: User has one of the roles prohibiting to accept.
 380              $this->sql->where .= ' AND (' . $this->sql_has_role($forbiddenroles);
 381              // Option 2: User has none of the roles allowing to accept.
 382              $this->sql->where .= ' OR ' . $this->sql_has_role($neededroles, false) . ")";
 383          }
 384      }
 385  
 386      /**
 387       * Returns SQL snippet for users that have (do not have) one of the given roles in the system context
 388       *
 389       * @param array $roles list of roles indexed by role id
 390       * @param bool $positive true: return users who HAVE roles; false: return users who DO NOT HAVE roles
 391       * @return string
 392       */
 393      protected function sql_has_role($roles, $positive = true) {
 394          global $CFG;
 395          if (empty($roles)) {
 396              return $positive ? '1=0' : '1=1';
 397          }
 398          $defaultuserroleid = isset($CFG->defaultuserroleid) ? $CFG->defaultuserroleid : 0;
 399          if (!empty($roles[$defaultuserroleid])) {
 400              // No need to query, everybody has the default role.
 401              return $positive ? '1=1' : '1=0';
 402          }
 403          return "u.id " . ($positive ? "" : "NOT") . " IN (
 404                  SELECT userid
 405                  FROM {role_assignments}
 406                  WHERE contextid = " . SYSCONTEXTID . " AND roleid IN (" . implode(',', array_keys($roles)) . ")
 407              )";
 408      }
 409  
 410      /**
 411       * If there is a filter by user roles add it to the SQL query.
 412       */
 413      protected function build_sql_for_roles_filter() {
 414          foreach ($this->acceptancesfilter->get_role_filters() as $roleid) {
 415              $this->sql->where .= ' AND ' . $this->sql_has_role([$roleid => $roleid]);
 416          }
 417      }
 418  
 419      /**
 420       * Hook that can be overridden in child classes to wrap a table in a form
 421       * for example. Called only when there is data to display and not
 422       * downloading.
 423       */
 424      public function wrap_html_start() {
 425          echo \html_writer::start_tag('form',
 426              ['action' => new \moodle_url('/admin/tool/policy/accept.php')]);
 427          echo \html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey()]);
 428          echo \html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'returnurl',
 429              'value' => $this->get_return_url()]);
 430          foreach (array_keys($this->versionids) as $versionid) {
 431              echo \html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'versionids[]',
 432                  'value' => $versionid]);
 433          }
 434      }
 435  
 436      /**
 437       * Hook that can be overridden in child classes to wrap a table in a form
 438       * for example. Called only when there is data to display and not
 439       * downloading.
 440       */
 441      public function wrap_html_finish() {
 442          global $PAGE;
 443          if ($this->canagreeany) {
 444              echo \html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'action', 'value' => 'accept']);
 445              echo \html_writer::empty_tag('input', ['type' => 'submit', 'data-action' => 'acceptmodal',
 446                  'value' => get_string('consentbulk', 'tool_policy'), 'class' => 'btn btn-primary mt-1']);
 447              $PAGE->requires->js_call_amd('tool_policy/acceptmodal', 'getInstance', [\context_system::instance()->id]);
 448          }
 449          echo "</form>\n";
 450      }
 451  
 452      /**
 453       * Render the table.
 454       */
 455      public function display() {
 456          $this->out(100, true);
 457      }
 458  
 459      /**
 460       * Call appropriate methods on this table class to perform any processing on values before displaying in table.
 461       * Takes raw data from the database and process it into human readable format, perhaps also adding html linking when
 462       * displaying table as html, adding a div wrap, etc.
 463       *
 464       * See for example col_fullname below which will be called for a column whose name is 'fullname'.
 465       *
 466       * @param array|object $row row of data from db used to make one row of the table.
 467       * @return array one row for the table, added using add_data_keyed method.
 468       */
 469      public function format_row($row) {
 470          \context_helper::preload_from_record($row);
 471          $row->canaccept = false;
 472          $row->user = \user_picture::unalias($row, [], $this->useridfield);
 473          $row->select = null;
 474          if (!$this->is_downloading()) {
 475              if (has_capability('tool/policy:acceptbehalf', \context_system::instance()) ||
 476                  has_capability('tool/policy:acceptbehalf', \context_user::instance($row->id))) {
 477                  $row->canaccept = true;
 478                  $row->select = \html_writer::empty_tag('input',
 479                      ['type' => 'checkbox', 'name' => 'userids[]', 'value' => $row->id, 'class' => 'usercheckbox',
 480                      'id' => 'selectuser' . $row->id]) .
 481                  \html_writer::tag('label', get_string('selectuser', 'tool_policy', $this->username($row->user, false)),
 482                      ['for' => 'selectuser' . $row->id, 'class' => 'accesshide']);
 483                  $this->canagreeany = true;
 484              }
 485          }
 486          return parent::format_row($row);
 487      }
 488  
 489      /**
 490       * Get the column fullname value.
 491       *
 492       * @param stdClass $row
 493       * @return string
 494       */
 495      public function col_fullname($row) {
 496          global $OUTPUT;
 497          $userpic = $this->is_downloading() ? '' : $OUTPUT->user_picture($row->user);
 498          return $userpic . $this->username($row->user, true);
 499      }
 500  
 501      /**
 502       * User name with a link to profile
 503       *
 504       * @param stdClass $user
 505       * @param bool $profilelink show link to profile (when we are downloading never show links)
 506       * @return string
 507       */
 508      protected function username($user, $profilelink = true) {
 509          $canviewfullnames = has_capability('moodle/site:viewfullnames', \context_system::instance()) ||
 510              has_capability('moodle/site:viewfullnames', \context_user::instance($user->id));
 511          $name = fullname($user, $canviewfullnames);
 512          if (!$this->is_downloading() && $profilelink) {
 513              $profileurl = new \moodle_url('/user/profile.php', array('id' => $user->id));
 514              return \html_writer::link($profileurl, $name);
 515          }
 516          return $name;
 517      }
 518  
 519      /**
 520       * Helper.
 521       */
 522      protected function get_return_url() {
 523          $pageurl = $this->baseurl;
 524          if ($this->currpage) {
 525              $pageurl = new \moodle_url($pageurl, [$this->request[TABLE_VAR_PAGE] => $this->currpage]);
 526          }
 527          return $pageurl;
 528      }
 529  
 530      /**
 531       * Return agreement status
 532       *
 533       * @param int $versionid either id of an individual version or empty for overall status
 534       * @param stdClass $row
 535       * @return string
 536       */
 537      protected function status($versionid, $row) {
 538          $onbehalf = false;
 539          $versions = $versionid ? [$versionid => $this->versionids[$versionid]] : $this->versionids; // List of versions.
 540          $accepted = []; // List of versionids that user has accepted.
 541          $declined = [];
 542  
 543          foreach ($versions as $v => $name) {
 544              if ($row->{'status' . $v} !== null) {
 545                  if (empty($row->{'status' . $v})) {
 546                      $declined[] = $v;
 547                  } else {
 548                      $accepted[] = $v;
 549                  }
 550                  $agreedby = $row->{'usermodified' . $v};
 551                  if ($agreedby && $agreedby != $row->id) {
 552                      $onbehalf = true;
 553                  }
 554              }
 555          }
 556  
 557          $ua = new user_agreement($row->id, $accepted, $declined, $this->get_return_url(), $versions, $onbehalf, $row->canaccept);
 558  
 559          if ($this->is_downloading()) {
 560              return $ua->export_for_download();
 561  
 562          } else {
 563              return $this->output->render($ua);
 564          }
 565      }
 566  
 567      /**
 568       * Get the column timemodified value.
 569       *
 570       * @param stdClass $row
 571       * @return string
 572       */
 573      public function col_timemodified($row) {
 574          if ($row->timemodified) {
 575              if ($this->is_downloading()) {
 576                  // Use timestamp format readable for both machines and humans.
 577                  return date_format_string((int) $row->timemodified, '%Y-%m-%d %H:%M:%S %Z');
 578              } else {
 579                  // Use localised calendar format.
 580                  return userdate($row->timemodified, get_string('strftimedatetime'));
 581              }
 582          } else {
 583              return null;
 584          }
 585      }
 586  
 587      /**
 588       * Get the column note value.
 589       *
 590       * @param stdClass $row
 591       * @return string
 592       */
 593      public function col_note($row) {
 594          if ($this->is_downloading()) {
 595              return $row->note;
 596          } else {
 597              return format_text($row->note, FORMAT_MOODLE);
 598          }
 599      }
 600  
 601      /**
 602       * Get the column statusall value.
 603       *
 604       * @param stdClass $row
 605       * @return string
 606       */
 607      public function col_statusall($row) {
 608          return $this->status(0, $row);
 609      }
 610  
 611      /**
 612       * Generate the country column.
 613       *
 614       * @param \stdClass $data
 615       * @return string
 616       */
 617      public function col_country($data) {
 618          if ($data->country && $this->countries === null) {
 619              $this->countries = get_string_manager()->get_list_of_countries();
 620          }
 621          if (!empty($this->countries[$data->country])) {
 622              return $this->countries[$data->country];
 623          }
 624          return '';
 625      }
 626  
 627      /**
 628       * You can override this method in a child class. See the description of
 629       * build_table which calls this method.
 630       *
 631       * @param string $column
 632       * @param stdClass $row
 633       * @return string
 634       */
 635      public function other_cols($column, $row) {
 636          if (preg_match('/^status([\d]+)$/', $column, $matches)) {
 637              $versionid = $matches[1];
 638              return $this->status($versionid, $row);
 639          }
 640          if (preg_match('/^usermodified([\d]+)$/', $column, $matches)) {
 641              if ($row->$column && $row->$column != $row->id) {
 642                  $user = (object)['id' => $row->$column];
 643                  username_load_fields_from_object($user, $row, 'mod');
 644                  return $this->username($user, true);
 645              }
 646              return ''; // User agreed by themselves.
 647          }
 648          return parent::other_cols($column, $row);
 649      }
 650  }