first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
@@ -0,0 +1,417 @@
<?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 coding_exception;
use core_reportbuilder\local\helpers\database;
use core_reportbuilder\local\report\column;
use core_reportbuilder\local\report\filter;
use lang_string;
/**
* Base class for all report entities
*
* @package core_reportbuilder
* @copyright 2019 Marina Glancy <marina@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class base {
/** @var string $entityname Internal reference to name of entity */
private $entityname = null;
/** @var lang_string $entitytitle Used as a title for the entity in reports */
private $entitytitle = null;
/** @var array $tablealiases Database tables that this entity uses and their aliases */
private $tablealiases = [];
/** @var array $tablejoinaliases Database tables that have already been joined to the report and their aliases */
private $tablejoinaliases = [];
/** @var string[] $joins List of SQL joins for the entity */
private $joins = [];
/** @var column[] $columns List of columns for the entity */
private $columns = [];
/** @var filter[] $filters List of filters for the entity */
private $filters = [];
/** @var filter[] $conditions List of conditions for the entity */
private $conditions = [];
/**
* Database tables that this entity uses
*
* Must be overridden by the entity to list all database tables that it expects to be present in the main
* SQL or in JOINs added to this entity
*
* @todo in Moodle 4.8 - make abstract when support for {@see get_default_table_aliases} is finally removed
*
* @return string[]
*/
protected function get_default_tables(): array {
static $debuggingshown;
// The default implementation falls back to retrieving deprecated table aliases to determine our table names.
$tablenamealiases = $this->get_default_table_aliases();
if (!empty($tablenamealiases) && !$debuggingshown) {
debugging('The function get_default_table_aliases() is deprecated, please define the entity' .
' tables with get_default_tables() in ' . static::class, DEBUG_DEVELOPER);
// Don't be too spammy with the debugging, this method is called multiple times per entity load.
$debuggingshown = true;
}
return array_keys($tablenamealiases);
}
/**
* Database tables that this entity uses and their default aliases (note that these aliases are now ignored)
*
* @return string[] Array of $tablename => $alias
*
* @deprecated since Moodle 4.4 - aliases are now autogenerated, please implement {@see get_default_tables} instead
*/
protected function get_default_table_aliases(): array {
return [];
}
/**
* The default title for this entity
*
* @return lang_string
*/
abstract protected function get_default_entity_title(): lang_string;
/**
* Initialise the entity, called automatically when it is added to a report
*
* This is where entity defines all its columns and filters by calling:
* - {@see add_column}
* - {@see add_filter}
* - etc
*
* @return self
*/
abstract public function initialise(): self;
/**
* The default machine-readable name for this entity that will be used in the internal names of the columns/filters
*
* @return string
*/
private function get_default_entity_name(): string {
$namespace = explode('\\', get_called_class());
return end($namespace);
}
/**
* Set entity name
*
* @param string $entityname
* @return self
*/
final public function set_entity_name(string $entityname): self {
$this->entityname = $entityname;
return $this;
}
/**
* Return entity name
*
* @return string
*/
final public function get_entity_name(): string {
return $this->entityname ?? $this->get_default_entity_name();
}
/**
* Set entity title
*
* @param lang_string $title
* @return self
*/
final public function set_entity_title(lang_string $title): self {
$this->entitytitle = $title;
return $this;
}
/**
* Get entity title
*
* @return lang_string
*/
final public function get_entity_title(): lang_string {
return $this->entitytitle ?? $this->get_default_entity_title();
}
/**
* Override the default alias for given database table used in entity queries, for instance when the same table is used
* by multiple entities and you want them each to refer to it by the same alias
*
* @param string $tablename One of the tables set by {@see get_default_tables}
* @param string $alias
* @return self
* @throws coding_exception For invalid table name
*/
final public function set_table_alias(string $tablename, string $alias): self {
$tablenames = $this->get_default_tables();
if (!in_array($tablename, $tablenames)) {
throw new coding_exception('Invalid table name', $tablename);
}
$this->tablealiases[$tablename] = $alias;
return $this;
}
/**
* Override multiple default database table aliases used in entity queries as per {@see set_table_alias}
*
* @param array $aliases Array of tablename => alias values
* @return self
*/
final public function set_table_aliases(array $aliases): self {
foreach ($aliases as $tablename => $alias) {
$this->set_table_alias($tablename, $alias);
}
return $this;
}
/**
* Returns an alias used in the queries for a given table
*
* @param string $tablename One of the tables set by {@see get_default_tables}
* @return string
* @throws coding_exception For invalid table name
*/
final public function get_table_alias(string $tablename): string {
$tablenames = $this->get_default_tables();
if (!in_array($tablename, $tablenames)) {
throw new coding_exception('Invalid table name', $tablename);
}
// We don't have the alias yet, generate a new one.
if (!array_key_exists($tablename, $this->tablealiases)) {
$this->set_table_alias($tablename, database::generate_alias());
}
return $this->tablealiases[$tablename];
}
/**
* Returns aliases used in the queries for all tables
*
* @return string[]
*/
final public function get_table_aliases(): array {
$tablenames = $this->get_default_tables();
return array_combine($tablenames, array_map([$this, 'get_table_alias'], $tablenames));
}
/**
* Set the alias for given database table that has already been added to the report. Enables entities to avoid additional
* joins on the same table by allowing re-use of existing table aliases in their own queries, {@see has_table_join_alias}
*
* @param string $tablename
* @param string $alias
* @return self
*/
final public function set_table_join_alias(string $tablename, string $alias): self {
$this->tablejoinaliases[$tablename] = $alias;
// Internally set the same table alias for the entity.
return $this->set_table_alias($tablename, $alias);
}
/**
* Determine whether defined table join alias was specified. Call {@see get_table_alias} to retrieve said value
*
* @param string $tablename
* @return bool
*/
final public function has_table_join_alias(string $tablename): bool {
return array_key_exists($tablename, $this->tablejoinaliases);
}
/**
* Add join clause required for this entity to join to existing tables/entities
*
* @param string $join
* @return self
*/
final public function add_join(string $join): self {
$this->joins[trim($join)] = trim($join);
return $this;
}
/**
* Add multiple join clauses required for this entity {@see add_join}
*
* @param string[] $joins
* @return self
*/
final public function add_joins(array $joins): self {
foreach ($joins as $join) {
$this->add_join($join);
}
return $this;
}
/**
* Return entity joins
*
* @return string[]
*/
final public function get_joins(): array {
return array_values($this->joins);
}
/**
* Helper method for returning joins necessary for retrieving tags related to the current entity
*
* Both 'tag' and 'tag_instance' aliases must be returned by the entity {@see get_default_tables} method
*
* @param string $component
* @param string $itemtype
* @param string $itemidfield
* @return string[]
*/
final protected function get_tag_joins_for_entity(string $component, string $itemtype, string $itemidfield): array {
$taginstancealias = $this->get_table_alias('tag_instance');
$tagalias = $this->get_table_alias('tag');
return [
"LEFT JOIN {tag_instance} {$taginstancealias}
ON {$taginstancealias}.component = '{$component}'
AND {$taginstancealias}.itemtype = '{$itemtype}'
AND {$taginstancealias}.itemid = {$itemidfield}",
"LEFT JOIN {tag} {$tagalias}
ON {$tagalias}.id = {$taginstancealias}.tagid",
];
}
/**
* Add a column to the entity
*
* @param column $column
* @return self
*/
final protected function add_column(column $column): self {
$this->columns[$column->get_name()] = $column;
return $this;
}
/**
* Returns entity columns
*
* @return column[]
*/
final public function get_columns(): array {
return $this->columns;
}
/**
* Returns an entity column
*
* @param string $name
* @return column
* @throws coding_exception For invalid column name
*/
final public function get_column(string $name): column {
if (!array_key_exists($name, $this->columns)) {
throw new coding_exception('Invalid column name', $name);
}
return $this->columns[$name];
}
/**
* Add a filter to the entity
*
* @param filter $filter
* @return self
*/
final protected function add_filter(filter $filter): self {
$this->filters[$filter->get_name()] = $filter;
return $this;
}
/**
* Returns entity filters
*
* @return filter[]
*/
final public function get_filters(): array {
return $this->filters;
}
/**
* Returns an entity filter
*
* @param string $name
* @return filter
* @throws coding_exception For invalid filter name
*/
final public function get_filter(string $name): filter {
if (!array_key_exists($name, $this->filters)) {
throw new coding_exception('Invalid filter name', $name);
}
return $this->filters[$name];
}
/**
* Add a condition to the entity
*
* @param filter $condition
* @return $this
*/
final protected function add_condition(filter $condition): self {
$this->conditions[$condition->get_name()] = $condition;
return $this;
}
/**
* Returns entity conditions
*
* @return filter[]
*/
final public function get_conditions(): array {
return $this->conditions;
}
/**
* Returns an entity condition
*
* @param string $name
* @return filter
* @throws coding_exception For invalid condition name
*/
final public function get_condition(string $name): filter {
if (!array_key_exists($name, $this->conditions)) {
throw new coding_exception('Invalid condition name', $name);
}
return $this->conditions[$name];
}
}
@@ -0,0 +1,483 @@
<?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_course;
use context_helper;
use core_reportbuilder\local\filters\boolean_select;
use core_reportbuilder\local\filters\course_selector;
use core_reportbuilder\local\filters\date;
use core_reportbuilder\local\filters\select;
use core_reportbuilder\local\filters\text;
use core_reportbuilder\local\helpers\custom_fields;
use core_reportbuilder\local\helpers\format;
use core_reportbuilder\local\report\column;
use core_reportbuilder\local\report\filter;
use html_writer;
use lang_string;
use stdClass;
use theme_config;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/course/lib.php');
/**
* Course entity class implementation
*
* This entity defines all the course columns and filters to be used in any report.
*
* @package core_reportbuilder
* @copyright 2021 Sara Arjona <sara@moodle.com> based on Marina Glancy code.
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class course extends base {
/**
* Database tables that this entity uses
*
* @return string[]
*/
protected function get_default_tables(): array {
return [
'course',
'context',
'tag_instance',
'tag',
];
}
/**
* The default title for this entity in the list of columns/filters in the report builder.
*
* @return lang_string
*/
protected function get_default_entity_title(): lang_string {
return new lang_string('entitycourse', 'core_reportbuilder');
}
/**
* Get custom fields helper
*
* @return custom_fields
*/
protected function get_custom_fields(): custom_fields {
$customfields = new custom_fields($this->get_table_alias('course') . '.id', $this->get_entity_name(),
'core_course', 'course');
$customfields->add_joins($this->get_joins());
return $customfields;
}
/**
* Initialise the entity, adding all course and custom course fields
*
* @return base
*/
public function initialise(): base {
$customfields = $this->get_custom_fields();
$columns = array_merge($this->get_all_columns(), $customfields->get_columns());
foreach ($columns as $column) {
$this->add_column($column);
}
$filters = array_merge($this->get_all_filters(), $customfields->get_filters());
foreach ($filters as $filter) {
$this
->add_condition($filter)
->add_filter($filter);
}
return $this;
}
/**
* Return syntax for joining on the context table
*
* @return string
*/
public function get_context_join(): string {
$coursealias = $this->get_table_alias('course');
$contextalias = $this->get_table_alias('context');
return "LEFT JOIN {context} {$contextalias}
ON {$contextalias}.contextlevel = " . CONTEXT_COURSE . "
AND {$contextalias}.instanceid = {$coursealias}.id";
}
/**
* Course fields.
*
* @return array
*/
protected function get_course_fields(): array {
return [
'fullname' => new lang_string('fullnamecourse'),
'shortname' => new lang_string('shortnamecourse'),
'idnumber' => new lang_string('idnumbercourse'),
'summary' => new lang_string('coursesummary'),
'format' => new lang_string('format'),
'startdate' => new lang_string('startdate'),
'enddate' => new lang_string('enddate'),
'visible' => new lang_string('coursevisibility'),
'groupmode' => new lang_string('groupmode', 'group'),
'groupmodeforce' => new lang_string('groupmodeforce', 'group'),
'lang' => new lang_string('forcelanguage'),
'calendartype' => new lang_string('forcecalendartype', 'calendar'),
'theme' => new lang_string('theme'),
'enablecompletion' => new lang_string('enablecompletion', 'completion'),
'downloadcontent' => new lang_string('downloadcoursecontent', 'course'),
'timecreated' => new lang_string('timecreated', 'core_reportbuilder'),
'timemodified' => new lang_string('timemodified', 'core_reportbuilder'),
];
}
/**
* 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 = [
'summary',
];
return !in_array($fieldname, $nonsortable);
}
/**
* Return appropriate column type for given user field
*
* @param string $coursefield
* @return int
*/
protected function get_course_field_type(string $coursefield): int {
switch ($coursefield) {
case 'downloadcontent':
case 'enablecompletion':
case 'groupmodeforce':
case 'visible':
$fieldtype = column::TYPE_BOOLEAN;
break;
case 'startdate':
case 'enddate':
case 'timecreated':
case 'timemodified':
$fieldtype = column::TYPE_TIMESTAMP;
break;
case 'summary':
$fieldtype = column::TYPE_LONGTEXT;
break;
case 'groupmode':
$fieldtype = column::TYPE_INTEGER;
break;
case 'calendartype':
case 'idnumber':
case 'format':
case 'fullname':
case 'lang':
case 'shortname':
case 'theme':
default:
$fieldtype = column::TYPE_TEXT;
break;
}
return $fieldtype;
}
/**
* Return joins necessary for retrieving tags
*
* @return string[]
*/
public function get_tag_joins(): array {
return $this->get_tag_joins_for_entity('core', 'course', $this->get_table_alias('course') . '.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;
$coursefields = $this->get_course_fields();
$tablealias = $this->get_table_alias('course');
$contexttablealias = $this->get_table_alias('context');
// Columns course full name with link, course short name with link and course id with link.
$fields = [
'coursefullnamewithlink' => 'fullname',
'courseshortnamewithlink' => 'shortname',
'courseidnumberewithlink' => 'idnumber',
];
foreach ($fields as $key => $field) {
$column = (new column(
$key,
new lang_string($key, 'core_reportbuilder'),
$this->get_entity_name()
))
->add_joins($this->get_joins())
->set_type(column::TYPE_TEXT)
->add_fields("{$tablealias}.{$field} as $key, {$tablealias}.id")
->set_is_sortable(true)
->add_callback(static function(?string $value, stdClass $row): string {
if ($value === null) {
return '';
}
context_helper::preload_from_record($row);
return html_writer::link(course_get_url($row->id),
format_string($value, true, ['context' => context_course::instance($row->id)]));
});
// Join on the context table so that we can use it for formatting these columns later.
if ($key === 'coursefullnamewithlink') {
$column->add_join($this->get_context_join())
->add_fields(context_helper::get_preload_record_columns_sql($contexttablealias));
}
$columns[] = $column;
}
foreach ($coursefields as $coursefield => $coursefieldlang) {
$columntype = $this->get_course_field_type($coursefield);
$columnfieldsql = "{$tablealias}.{$coursefield}";
if ($columntype === column::TYPE_LONGTEXT && $DB->get_dbfamily() === 'oracle') {
$columnfieldsql = $DB->sql_order_by_text($columnfieldsql, 1024);
}
$column = (new column(
$coursefield,
$coursefieldlang,
$this->get_entity_name()
))
->add_joins($this->get_joins())
->set_type($columntype)
->add_field($columnfieldsql, $coursefield)
->add_callback([$this, 'format'], $coursefield)
->set_is_sortable($this->is_sortable($coursefield));
// Join on the context table so that we can use it for formatting these columns later.
if ($coursefield === 'summary' || $coursefield === 'shortname' || $coursefield === 'fullname') {
$column->add_join($this->get_context_join())
->add_field("{$tablealias}.id", 'courseid')
->add_fields(context_helper::get_preload_record_columns_sql($contexttablealias));
}
$columns[] = $column;
}
return $columns;
}
/**
* Returns list of all available filters
*
* @return array
*/
protected function get_all_filters(): array {
global $DB;
$filters = [];
$tablealias = $this->get_table_alias('course');
$fields = $this->get_course_fields();
foreach ($fields as $field => $name) {
$filterfieldsql = "{$tablealias}.{$field}";
if ($this->get_course_field_type($field) === column::TYPE_LONGTEXT) {
$filterfieldsql = $DB->sql_cast_to_char($filterfieldsql);
}
$optionscallback = [static::class, 'get_options_for_' . $field];
if (is_callable($optionscallback)) {
$filterclass = select::class;
} else if ($this->get_course_field_type($field) === column::TYPE_BOOLEAN) {
$filterclass = boolean_select::class;
} else if ($this->get_course_field_type($field) === column::TYPE_TIMESTAMP) {
$filterclass = date::class;
} else {
$filterclass = text::class;
}
$filter = (new filter(
$filterclass,
$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;
}
// We add our own custom course selector filter.
$filters[] = (new filter(
course_selector::class,
'courseselector',
new lang_string('courseselect', 'core_reportbuilder'),
$this->get_entity_name(),
"{$tablealias}.id"
))
->add_joins($this->get_joins());
return $filters;
}
/**
* Gets list of options if the filter supports it
*
* @param string $fieldname
* @return null|array
*/
protected function get_options_for(string $fieldname): ?array {
static $cached = [];
if (!array_key_exists($fieldname, $cached)) {
$callable = [static::class, 'get_options_for_' . $fieldname];
if (is_callable($callable)) {
$cached[$fieldname] = $callable();
} else {
$cached[$fieldname] = null;
}
}
return $cached[$fieldname];
}
/**
* List of options for the field groupmode.
*
* @return array
*/
public static function get_options_for_groupmode(): array {
return [
NOGROUPS => get_string('groupsnone', 'group'),
SEPARATEGROUPS => get_string('groupsseparate', 'group'),
VISIBLEGROUPS => get_string('groupsvisible', 'group'),
];
}
/**
* List of options for the field format.
*
* @return array
*/
public static function get_options_for_format(): array {
global $CFG;
require_once($CFG->dirroot.'/course/lib.php');
$options = [];
$courseformats = get_sorted_course_formats(true);
foreach ($courseformats as $courseformat) {
$options[$courseformat] = get_string('pluginname', "format_{$courseformat}");
}
return $options;
}
/**
* List of options for the field theme.
*
* @return array
*/
public static function get_options_for_theme(): array {
return array_map(
fn(theme_config $theme) => $theme->get_theme_name(),
get_list_of_themes(),
);
}
/**
* List of options for the field lang.
*
* @return array
*/
public static function get_options_for_lang(): array {
return get_string_manager()->get_list_of_translations();
}
/**
* List of options for the field.
*
* @return array
*/
public static function get_options_for_calendartype(): array {
return \core_calendar\type_factory::get_list_of_calendar_types();
}
/**
* Formats the course 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 {
if ($this->get_course_field_type($fieldname) === column::TYPE_TIMESTAMP) {
return format::userdate($value, $row);
}
if ($this->get_course_field_type($fieldname) === column::TYPE_BOOLEAN) {
return format::boolean_as_text($value);
}
// If the column has corresponding filter, determine the value from its options.
$options = $this->get_options_for($fieldname);
if ($options !== null && array_key_exists($value, $options)) {
return $options[$value];
}
if (in_array($fieldname, ['fullname', 'shortname'])) {
if (!$row->courseid) {
return '';
}
context_helper::preload_from_record($row);
$context = context_course::instance($row->courseid);
return format_string($value, true, ['context' => $context->id, 'escape' => false]);
}
if (in_array($fieldname, ['summary'])) {
if (!$row->courseid) {
return '';
}
context_helper::preload_from_record($row);
$context = context_course::instance($row->courseid);
$summary = file_rewrite_pluginfile_urls($row->summary, 'pluginfile.php', $context->id, 'course', 'summary', null);
return format_text($summary);
}
return s($value);
}
}
@@ -0,0 +1,610 @@
<?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\context;
use core_component;
use html_writer;
use lang_string;
use moodle_url;
use stdClass;
use theme_config;
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
*
* @return string[]
*/
protected function get_default_tables(): array {
return [
'user',
'context',
'tag_instance',
'tag',
];
}
/**
* 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 columns that correspond to the site configured identity fields
*
* @param context $context
* @param string[] $excluding
* @return column[]
*/
public function get_identity_columns(context $context, array $excluding = []): array {
$identityfields = fields::for_identity($context)->excluding(...$excluding)->get_required_fields();
return array_map([$this, 'get_identity_column'], $identityfields);
}
/**
* 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);
}
/**
* Returns filters that correspond to the site configured identity fields
*
* @param context $context
* @param string[] $excluding
* @return filter[]
*/
public function get_identity_filters(context $context, array $excluding = []): array {
$identityfields = fields::for_identity($context)->excluding(...$excluding)->get_required_fields();
return array_map([$this, 'get_identity_filter'], $identityfields);
}
/**
* Return joins necessary for retrieving tags
*
* @return string[]
*/
public function get_tag_joins(): array {
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);
// Join on the context table so that we can use it for formatting these columns later.
if ($userfield === 'description') {
$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 the column has corresponding filter, determine the value from its options.
$options = $this->get_options_for($fieldname);
if ($options !== null && array_key_exists($value, $options)) {
return $options[$value];
}
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);
$viewfullnames = has_capability('moodle/site:viewfullnames', context_system::instance());
$dummyfullname = fullname($dummyuser, $viewfullnames);
// 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'),
'theme' => new lang_string('theme'),
'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'),
'auth' => new lang_string('authentication', 'moodle'),
'moodlenetprofile' => new lang_string('moodlenetprofile', 'user'),
'timecreated' => new lang_string('timecreated', 'core_reportbuilder'),
'timemodified' => new lang_string('timemodified', 'core_reportbuilder'),
'lastip' => new lang_string('lastip'),
];
}
/**
* 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':
case 'timemodified':
$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());
return $filters;
}
/**
* Gets list of options if the filter supports it
*
* @param string $fieldname
* @return null|array
*/
protected function get_options_for(string $fieldname): ?array {
static $cached = [];
if (!array_key_exists($fieldname, $cached)) {
$callable = [static::class, 'get_options_for_' . $fieldname];
if (is_callable($callable)) {
$cached[$fieldname] = $callable();
} else {
$cached[$fieldname] = null;
}
}
return $cached[$fieldname];
}
/**
* List of options for the field auth
*
* @return string[]
*/
public static function get_options_for_auth(): array {
$authlist = array_keys(core_component::get_plugin_list('auth'));
return array_map(
fn(string $auth) => get_auth_plugin($auth)->get_title(),
array_combine($authlist, $authlist),
);
}
/**
* List of options for the field country.
*
* @return string[]
*/
public static function get_options_for_country(): array {
return get_string_manager()->get_list_of_countries();
}
/**
* List of options for the field theme.
*
* @return string[]
*/
public static function get_options_for_theme(): array {
return array_map(
fn(theme_config $theme) => $theme->get_theme_name(),
get_list_of_themes(),
);
}
}