<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\local\entities;
use context_helper;
use context_system;
use context_user;
use core_component;
use html_writer;
use lang_string;
use moodle_url;
use stdClass;
use core_user\fields;
use core_reportbuilder\local\filters\boolean_select;
use core_reportbuilder\local\filters\date;
use core_reportbuilder\local\filters\select;
use core_reportbuilder\local\filters\text;
use core_reportbuilder\local\filters\user as user_filter;
use core_reportbuilder\local\helpers\user_profile_fields;
use core_reportbuilder\local\helpers\format;
use core_reportbuilder\local\report\column;
use core_reportbuilder\local\report\filter;
/**
* User entity class implementation.
*
* This entity defines all the user columns and filters to be used in any report.
*
* @package core_reportbuilder
* @copyright 2020 Sara Arjona <sara@moodle.com> based on Marina Glancy code.
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class user extends base {
/**
* Database tables that this entity uses and their default aliases
*
* @return array
*/
protected function get_default_table_aliases(): array {
return [
'user' => 'u',
'context' => 'uctx',
'tag_instance' => 'uti',
'tag' => 'ut',
];
}
/**
* The default title for this entity
*
* @return lang_string
*/
protected function get_default_entity_title(): lang_string {
return new lang_string('entityuser', 'core_reportbuilder');
}
/**
* Initialise the entity, add all user fields and all 'visible' user profile fields
*
* @return base
*/
public function initialise(): base {
$userprofilefields = $this->get_user_profile_fields();
$columns = array_merge($this->get_all_columns(), $userprofilefields->get_columns());
foreach ($columns as $column) {
$this->add_column($column);
}
$filters = array_merge($this->get_all_filters(), $userprofilefields->get_filters());
foreach ($filters as $filter) {
$this->add_filter($filter);
}
$conditions = array_merge($this->get_all_filters(), $userprofilefields->get_filters());
foreach ($conditions as $condition) {
$this->add_condition($condition);
}
return $this;
}
/**
* Get user profile fields helper instance
*
* @return user_profile_fields
*/
protected function get_user_profile_fields(): user_profile_fields {
$userprofilefields = new user_profile_fields($this->get_table_alias('user') . '.id', $this->get_entity_name());
$userprofilefields->add_joins($this->get_joins());
return $userprofilefields;
}
/**
* Returns column that corresponds to the given identity field, profile field identifiers will be converted to those
* used by the {@see user_profile_fields} helper
*
* @param string $identityfield Field from the user table, or a custom profile field
* @return column
*/
public function get_identity_column(string $identityfield): column {
if (preg_match(fields::PROFILE_FIELD_REGEX, $identityfield, $matches)) {
$identityfield = 'profilefield_' . $matches[1];
}
return $this->get_column($identityfield);
}
/**
* Returns filter that corresponds to the given identity field, profile field identifiers will be converted to those
* used by the {@see user_profile_fields} helper
*
* @param string $identityfield Field from the user table, or a custom profile field
* @return filter
*/
public function get_identity_filter(string $identityfield): filter {
if (preg_match(fields::PROFILE_FIELD_REGEX, $identityfield, $matches)) {
$identityfield = 'profilefield_' . $matches[1];
}
return $this->get_filter($identityfield);
}
/**
* Return joins necessary for retrieving tags
*
* @return string[]
*/
public function get_tag_joins(): array {
< $user = $this->get_table_alias('user');
< $taginstance = $this->get_table_alias('tag_instance');
< $tag = $this->get_table_alias('tag');
<
< return [
< "LEFT JOIN {tag_instance} {$taginstance}
< ON {$taginstance}.component = 'core'
< AND {$taginstance}.itemtype = 'user'
< AND {$taginstance}.itemid = {$user}.id",
< "LEFT JOIN {tag} {$tag}
< ON {$tag}.id = {$taginstance}.tagid",
< ];
> return $this->get_tag_joins_for_entity('core', 'user', $this->get_table_alias('user') . '.id');
}
/**
* Returns list of all available columns
*
* These are all the columns available to use in any report that uses this entity.
*
* @return column[]
*/
protected function get_all_columns(): array {
global $DB;
$usertablealias = $this->get_table_alias('user');
$contexttablealias = $this->get_table_alias('context');
$fullnameselect = self::get_name_fields_select($usertablealias);
$fullnamesort = explode(', ', $fullnameselect);
$userpictureselect = fields::for_userpic()->get_sql($usertablealias, false, '', '', false)->selects;
$viewfullnames = has_capability('moodle/site:viewfullnames', context_system::instance());
// Fullname column.
$columns[] = (new column(
'fullname',
new lang_string('fullname'),
$this->get_entity_name()
))
->add_joins($this->get_joins())
->add_fields($fullnameselect)
->set_type(column::TYPE_TEXT)
->set_is_sortable($this->is_sortable('fullname'), $fullnamesort)
->add_callback(static function(?string $value, stdClass $row) use ($viewfullnames): string {
if ($value === null) {
return '';
}
// Ensure we populate all required name properties.
$namefields = fields::get_name_fields();
foreach ($namefields as $namefield) {
$row->{$namefield} = $row->{$namefield} ?? '';
}
return fullname($row, $viewfullnames);
});
// Formatted fullname columns (with link, picture or both).
$fullnamefields = [
'fullnamewithlink' => new lang_string('userfullnamewithlink', 'core_reportbuilder'),
'fullnamewithpicture' => new lang_string('userfullnamewithpicture', 'core_reportbuilder'),
'fullnamewithpicturelink' => new lang_string('userfullnamewithpicturelink', 'core_reportbuilder'),
];
foreach ($fullnamefields as $fullnamefield => $fullnamelang) {
$column = (new column(
$fullnamefield,
$fullnamelang,
$this->get_entity_name()
))
->add_joins($this->get_joins())
->add_fields($fullnameselect)
->add_field("{$usertablealias}.id")
->set_type(column::TYPE_TEXT)
->set_is_sortable($this->is_sortable($fullnamefield), $fullnamesort)
->add_callback(static function(?string $value, stdClass $row) use ($fullnamefield, $viewfullnames): string {
global $OUTPUT;
if ($value === null) {
return '';
}
// Ensure we populate all required name properties.
$namefields = fields::get_name_fields();
foreach ($namefields as $namefield) {
$row->{$namefield} = $row->{$namefield} ?? '';
}
if ($fullnamefield === 'fullnamewithlink') {
return html_writer::link(new moodle_url('/user/profile.php', ['id' => $row->id]),
fullname($row, $viewfullnames));
}
if ($fullnamefield === 'fullnamewithpicture') {
return $OUTPUT->user_picture($row, ['link' => false, 'alttext' => false]) .
fullname($row, $viewfullnames);
}
if ($fullnamefield === 'fullnamewithpicturelink') {
return html_writer::link(new moodle_url('/user/profile.php', ['id' => $row->id]),
$OUTPUT->user_picture($row, ['link' => false, 'alttext' => false]) .
fullname($row, $viewfullnames));
}
return $value;
});
// Picture fields need some more data.
if (strpos($fullnamefield, 'picture') !== false) {
$column->add_fields($userpictureselect);
}
$columns[] = $column;
}
// Picture column.
$columns[] = (new column(
'picture',
new lang_string('userpicture', 'core_reportbuilder'),
$this->get_entity_name()
))
->add_joins($this->get_joins())
->add_fields($userpictureselect)
->set_type(column::TYPE_INTEGER)
->set_is_sortable($this->is_sortable('picture'))
// It doesn't make sense to offer integer aggregation methods for this column.
->set_disabled_aggregation(['avg', 'max', 'min', 'sum'])
->add_callback(static function ($value, stdClass $row): string {
global $OUTPUT;
return !empty($row->id) ? $OUTPUT->user_picture($row, ['link' => false, 'alttext' => false]) : '';
});
// Add all other user fields.
$userfields = $this->get_user_fields();
foreach ($userfields as $userfield => $userfieldlang) {
$columntype = $this->get_user_field_type($userfield);
$columnfieldsql = "{$usertablealias}.{$userfield}";
if ($columntype === column::TYPE_LONGTEXT && $DB->get_dbfamily() === 'oracle') {
$columnfieldsql = $DB->sql_order_by_text($columnfieldsql, 1024);
}
$column = (new column(
$userfield,
$userfieldlang,
$this->get_entity_name()
))
->add_joins($this->get_joins())
->set_type($columntype)
->add_field($columnfieldsql, $userfield)
->set_is_sortable($this->is_sortable($userfield))
->add_callback([$this, 'format'], $userfield);
// Some columns also have specific format callbacks.
if ($userfield === 'country') {
$column->add_callback(static function(string $country): string {
$countries = get_string_manager()->get_list_of_countries(true);
return $countries[$country] ?? '';
});
} else if ($userfield === 'description') {
// Select enough fields in order to format the column.
$column
->add_join("LEFT JOIN {context} {$contexttablealias}
ON {$contexttablealias}.contextlevel = " . CONTEXT_USER . "
AND {$contexttablealias}.instanceid = {$usertablealias}.id")
->add_fields("{$usertablealias}.descriptionformat, {$usertablealias}.id")
->add_fields(context_helper::get_preload_record_columns_sql($contexttablealias));
}
$columns[] = $column;
}
return $columns;
}
/**
* Check if this field is sortable
*
* @param string $fieldname
* @return bool
*/
protected function is_sortable(string $fieldname): bool {
// Some columns can't be sorted, like longtext or images.
$nonsortable = [
'description',
'picture',
];
return !in_array($fieldname, $nonsortable);
}
/**
* Formats the user field for display.
*
* @param mixed $value Current field value.
* @param stdClass $row Complete row.
* @param string $fieldname Name of the field to format.
* @return string
*/
public function format($value, stdClass $row, string $fieldname): string {
global $CFG;
if ($this->get_user_field_type($fieldname) === column::TYPE_BOOLEAN) {
return format::boolean_as_text($value);
}
if ($this->get_user_field_type($fieldname) === column::TYPE_TIMESTAMP) {
return format::userdate($value, $row);
}
if ($fieldname === 'description') {
if (empty($row->id)) {
return '';
}
require_once("{$CFG->libdir}/filelib.php");
context_helper::preload_from_record($row);
$context = context_user::instance($row->id);
$description = file_rewrite_pluginfile_urls($value, 'pluginfile.php', $context->id, 'user', 'profile', null);
return format_text($description, $row->descriptionformat, ['context' => $context->id]);
}
return s($value);
}
/**
* Returns a SQL statement to select all user fields necessary for fullname() function
*
* Note the implementation here is similar to {@see fields::get_sql_fullname} but without concatenation
*
* @param string $usertablealias
* @return string
*/
public static function get_name_fields_select(string $usertablealias = 'u'): string {
$namefields = fields::get_name_fields(true);
// Create a dummy user object containing all name fields.
$dummyuser = (object) array_combine($namefields, $namefields);
$dummyfullname = fullname($dummyuser, true);
// Extract any name fields from the fullname format in the order that they appear.
$matchednames = array_values(order_in_string($namefields, $dummyfullname));
$userfields = array_map(static function(string $userfield) use ($usertablealias): string {
if (!empty($usertablealias)) {
$userfield = "{$usertablealias}.{$userfield}";
}
return $userfield;
}, $matchednames);
return implode(', ', $userfields);
}
/**
* User fields
*
* @return lang_string[]
*/
protected function get_user_fields(): array {
return [
'firstname' => new lang_string('firstname'),
'lastname' => new lang_string('lastname'),
'email' => new lang_string('email'),
'city' => new lang_string('city'),
'country' => new lang_string('country'),
'description' => new lang_string('description'),
'firstnamephonetic' => new lang_string('firstnamephonetic'),
'lastnamephonetic' => new lang_string('lastnamephonetic'),
'middlename' => new lang_string('middlename'),
'alternatename' => new lang_string('alternatename'),
'idnumber' => new lang_string('idnumber'),
'institution' => new lang_string('institution'),
'department' => new lang_string('department'),
'phone1' => new lang_string('phone1'),
'phone2' => new lang_string('phone2'),
'address' => new lang_string('address'),
'lastaccess' => new lang_string('lastaccess'),
'suspended' => new lang_string('suspended'),
'confirmed' => new lang_string('confirmed', 'admin'),
'username' => new lang_string('username'),
'moodlenetprofile' => new lang_string('moodlenetprofile', 'user'),
'timecreated' => new lang_string('timecreated', 'core_reportbuilder'),
];
}
/**
* Return appropriate column type for given user field
*
* @param string $userfield
* @return int
*/
protected function get_user_field_type(string $userfield): int {
switch ($userfield) {
case 'description':
$fieldtype = column::TYPE_LONGTEXT;
break;
case 'confirmed':
case 'suspended':
$fieldtype = column::TYPE_BOOLEAN;
break;
case 'lastaccess':
case 'timecreated':
$fieldtype = column::TYPE_TIMESTAMP;
break;
default:
$fieldtype = column::TYPE_TEXT;
break;
}
return $fieldtype;
}
/**
* Return list of all available filters
*
* @return filter[]
*/
protected function get_all_filters(): array {
global $DB;
$filters = [];
$tablealias = $this->get_table_alias('user');
// Fullname filter.
$canviewfullnames = has_capability('moodle/site:viewfullnames', context_system::instance());
[$fullnamesql, $fullnameparams] = fields::get_sql_fullname($tablealias, $canviewfullnames);
$filters[] = (new filter(
text::class,
'fullname',
new lang_string('fullname'),
$this->get_entity_name(),
$fullnamesql,
$fullnameparams
))
->add_joins($this->get_joins());
// User fields filters.
$fields = $this->get_user_fields();
foreach ($fields as $field => $name) {
$filterfieldsql = "{$tablealias}.{$field}";
if ($this->get_user_field_type($field) === column::TYPE_LONGTEXT) {
$filterfieldsql = $DB->sql_cast_to_char($filterfieldsql);
}
$optionscallback = [static::class, 'get_options_for_' . $field];
if (is_callable($optionscallback)) {
$classname = select::class;
} else if ($this->get_user_field_type($field) === column::TYPE_BOOLEAN) {
$classname = boolean_select::class;
} else if ($this->get_user_field_type($field) === column::TYPE_TIMESTAMP) {
$classname = date::class;
} else {
$classname = text::class;
}
$filter = (new filter(
$classname,
$field,
$name,
$this->get_entity_name(),
$filterfieldsql
))
->add_joins($this->get_joins());
// Populate filter options by callback, if available.
if (is_callable($optionscallback)) {
$filter->set_options_callback($optionscallback);
}
$filters[] = $filter;
}
// User select filter.
$filters[] = (new filter(
user_filter::class,
'userselect',
new lang_string('userselect', 'core_reportbuilder'),
$this->get_entity_name(),
"{$tablealias}.id"
))
->add_joins($this->get_joins());
// Authentication method filter.
$filters[] = (new filter(
select::class,
'auth',
new lang_string('authentication', 'moodle'),
$this->get_entity_name(),
"{$tablealias}.auth"
))
->add_joins($this->get_joins())
->set_options_callback(static function(): array {
$plugins = core_component::get_plugin_list('auth');
$enabled = get_string('pluginenabled', 'core_plugin');
$disabled = get_string('plugindisabled', 'core_plugin');
$authoptions = [$enabled => [], $disabled => []];
foreach ($plugins as $pluginname => $unused) {
$plugin = get_auth_plugin($pluginname);
if (is_enabled_auth($pluginname)) {
$authoptions[$enabled][$pluginname] = $plugin->get_title();
} else {
$authoptions[$disabled][$pluginname] = $plugin->get_title();
}
}
return $authoptions;
});
return $filters;
}
/**
* List of options for the field country.
*
* @return string[]
*/
public static function get_options_for_country(): array {
return array_map('shorten_text', get_string_manager()->get_list_of_countries());
}
}