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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body