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,89 @@
<?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\helpers;
use core_collator;
use core_component;
use core_reportbuilder\local\aggregation\base;
/**
* Helper class for column aggregation related methods
*
* @package core_reportbuilder
* @copyright 2021 Paul Holden <paulh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class aggregation {
/**
* Helper method to convert aggregation class name into fully qualified namespaced class
*
* @param string $aggregation
* @return string
*/
public static function get_full_classpath(string $aggregation): string {
return "\\core_reportbuilder\\local\\aggregation\\{$aggregation}";
}
/**
* Validate whether given class is a valid aggregation type
*
* @param string $aggregationclass Fully qualified namespaced class, see {@see get_full_classpath} for converting value
* stored in column persistent to full path
* @return bool
*/
public static function valid(string $aggregationclass): bool {
return class_exists($aggregationclass) && is_subclass_of($aggregationclass, base::class);
}
/**
* Return list of all available/valid aggregation types
*
* @return base[]
*/
public static function get_aggregations(): array {
$classes = core_component::get_component_classes_in_namespace('core_reportbuilder', 'local\\aggregation');
return array_filter(array_keys($classes), static function(string $class): bool {
return static::valid($class);
});
}
/**
* Get available aggregation types for given column type
*
* @param int $columntype
* @param array $exclude List of types to exclude, e.g. ['min', 'sum']
* @return string[] Aggregation types indexed by [shortname => name]
*/
public static function get_column_aggregations(int $columntype, array $exclude = []): array {
$types = [];
$classes = static::get_aggregations();
foreach ($classes as $class) {
if ($class::compatible($columntype) && !in_array($class::get_class_name(), $exclude)) {
$types[$class::get_class_name()] = (string) $class::get_name();
}
}
core_collator::asort($types, core_collator::SORT_STRING);
return $types;
}
}
@@ -0,0 +1,320 @@
<?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\helpers;
use cache;
use context;
use context_system;
use core_collator;
use core_component;
use core_reportbuilder\local\audiences\base;
use core_reportbuilder\local\models\{audience as audience_model, schedule};
/**
* Class containing report audience helper methods
*
* @package core_reportbuilder
* @copyright 2021 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class audience {
/**
* Return audience instances for a given report. Note that any records pointing to invalid audience types will be excluded
*
* @param int $reportid
* @return base[]
*/
public static function get_base_records(int $reportid): array {
$records = audience_model::get_records(['reportid' => $reportid], 'id');
$instances = array_map(static function(audience_model $audience): ?base {
return base::instance(0, $audience->to_record());
}, $records);
// Filter and remove null elements (invalid audience types).
return array_filter($instances);
}
/**
* Returns list of report IDs that the specified user can access, based on audience configuration. This can be expensive if the
* site has lots of reports, with lots of audiences, so we cache the result for the duration of the users session
*
* @param int|null $userid User ID to check, or the current user if omitted
* @return int[]
*/
public static function get_allowed_reports(?int $userid = null): array {
global $USER, $DB;
$userid = $userid ?: (int) $USER->id;
// Prepare cache, if we previously stored the users allowed reports then return that.
$cache = cache::make('core', 'reportbuilder_allowed_reports');
$cachedreports = $cache->get($userid);
if ($cachedreports !== false) {
return $cachedreports;
}
$allowedreports = [];
$reportaudiences = [];
// Retrieve all audiences and group them by report for convenience.
$audiences = audience_model::get_records();
foreach ($audiences as $audience) {
$reportaudiences[$audience->get('reportid')][] = $audience;
}
foreach ($reportaudiences as $reportid => $audiences) {
// Generate audience SQL based on those for the current report.
[$wheres, $params] = self::user_audience_sql($audiences);
if (count($wheres) === 0) {
continue;
}
$paramuserid = database::generate_param_name();
$params[$paramuserid] = $userid;
$sql = "SELECT DISTINCT(u.id)
FROM {user} u
WHERE (" . implode(' OR ', $wheres) . ")
AND u.deleted = 0
AND u.id = :{$paramuserid}";
// If we have a matching record, user can view the report.
if ($DB->record_exists_sql($sql, $params)) {
$allowedreports[] = $reportid;
}
}
// Store users allowed reports in cache.
$cache->set($userid, $allowedreports);
return $allowedreports;
}
/**
* Purge the audience cache of allowed reports
*/
public static function purge_caches(): void {
cache::make('core', 'reportbuilder_allowed_reports')->purge();
}
/**
* Generate SQL select clause and params for selecting reports specified user can access, based on audience configuration
*
* @param string $reporttablealias
* @param int|null $userid User ID to check, or the current user if omitted
* @return array
*/
public static function user_reports_list_sql(string $reporttablealias, ?int $userid = null): array {
global $DB;
$allowedreports = self::get_allowed_reports($userid);
if (empty($allowedreports)) {
return ['1=0', []];
}
// Get all sql audiences.
[$select, $params] = $DB->get_in_or_equal($allowedreports, SQL_PARAMS_NAMED, database::generate_param_name('_'));
$sql = "{$reporttablealias}.id {$select}";
return [$sql, $params];
}
/**
* Return list of report ID's specified user can access, based on audience configuration
*
* @param int|null $userid User ID to check, or the current user if omitted
* @return int[]
*/
public static function user_reports_list(?int $userid = null): array {
global $DB;
[$select, $params] = self::user_reports_list_sql('rb', $userid);
$sql = "SELECT rb.id
FROM {reportbuilder_report} rb
WHERE {$select}";
return $DB->get_fieldset_sql($sql, $params);
}
/**
* Returns SQL to limit the list of reports to those that the given user has access to
*
* - A user with 'viewall/editall' capability will have access to all reports
* - A user with 'edit' capability will have access to:
* - Those reports this user has created
* - Those reports this user is in audience of
* - Otherwise:
* - Those reports this user is in audience of
*
* @param string $reporttablealias
* @param int|null $userid User ID to check, or the current user if omitted
* @param context|null $context
* @return array
*/
public static function user_reports_list_access_sql(
string $reporttablealias,
?int $userid = null,
?context $context = null
): array {
global $DB, $USER;
if ($context === null) {
$context = context_system::instance();
}
if (has_any_capability(['moodle/reportbuilder:editall', 'moodle/reportbuilder:viewall'], $context, $userid)) {
return ['1=1', []];
}
// Limit the returned list to those reports the user can see, by selecting based on report audience.
[$reportselect, $params] = $DB->get_in_or_equal(
self::user_reports_list($userid),
SQL_PARAMS_NAMED,
database::generate_param_name('_'),
true,
null,
);
$where = "{$reporttablealias}.id {$reportselect}";
// User can also see any reports that they can edit.
if (has_capability('moodle/reportbuilder:edit', $context, $userid)) {
$paramuserid = database::generate_param_name();
$where = "({$reporttablealias}.usercreated = :{$paramuserid} OR {$where})";
$params[$paramuserid] = $userid ?? $USER->id;
}
return [$where, $params];
}
/**
* Return appropriate list of where clauses and params for given audiences
*
* @param audience_model[] $audiences
* @param string $usertablealias
* @return array[] [$wheres, $params]
*/
public static function user_audience_sql(array $audiences, string $usertablealias = 'u'): array {
$wheres = $params = [];
foreach ($audiences as $audience) {
if ($instance = base::instance(0, $audience->to_record())) {
$instancetablealias = database::generate_alias();
[$instancejoin, $instancewhere, $instanceparams] = $instance->get_sql($instancetablealias);
$wheres[] = "{$usertablealias}.id IN (
SELECT {$instancetablealias}.id
FROM {user} {$instancetablealias}
{$instancejoin}
WHERE {$instancewhere}
)";
$params += $instanceparams;
}
}
return [$wheres, $params];
}
/**
* Return a list of audiences that are used by any schedule of the given report
*
* @param int $reportid
* @return int[] Array of audience IDs
*/
public static function get_audiences_for_report_schedules(int $reportid): array {
global $DB;
$audiences = $DB->get_fieldset_select(schedule::TABLE, 'audiences', 'reportid = ?', [$reportid]);
// Reduce JSON encoded audience data of each schedule to an array of audience IDs.
$audienceids = array_reduce($audiences, static function(array $carry, string $audience): array {
return array_merge($carry, (array) json_decode($audience));
}, []);
return array_unique($audienceids, SORT_NUMERIC);
}
/**
* Returns the list of audiences types in the system.
*
* @return array
*/
private static function get_audience_types(): array {
$sources = [];
$audiences = core_component::get_component_classes_in_namespace(null, 'reportbuilder\\audience');
foreach ($audiences as $class => $path) {
$audienceclass = $class::instance();
if (is_subclass_of($class, base::class) && $audienceclass->user_can_add()) {
$componentname = $audienceclass->get_component_displayname();
$sources[$componentname][$class] = $audienceclass->get_name();
}
}
return $sources;
}
/**
* Get all the audiences types the current user can add to, organised by categories.
*
* @return array
*
* @deprecated since Moodle 4.1 - please do not use this function any more, {@see custom_report_audience_cards_exporter}
*/
public static function get_all_audiences_menu_types(): array {
debugging('The function ' . __FUNCTION__ . '() is deprecated, please do not use it any more. ' .
'See \'custom_report_audience_cards_exporter\' class for replacement', DEBUG_DEVELOPER);
$menucardsarray = [];
$notavailablestr = get_string('notavailable', 'moodle');
$audiencetypes = self::get_audience_types();
$audiencetypeindex = 0;
foreach ($audiencetypes as $categoryname => $audience) {
$menucards = [
'name' => $categoryname,
'key' => 'index' . ++$audiencetypeindex,
];
foreach ($audience as $classname => $name) {
$class = $classname::instance();
$title = $class->is_available() ? get_string('addaudience', 'core_reportbuilder', $class->get_name()) :
$notavailablestr;
$menucard['title'] = $title;
$menucard['name'] = $class->get_name();
$menucard['disabled'] = !$class->is_available();
$menucard['identifier'] = get_class($class);
$menucard['action'] = 'add-audience';
$menucards['items'][] = $menucard;
}
// Order audience types on each category alphabetically.
core_collator::asort_array_of_arrays_by_key($menucards['items'], 'name');
$menucards['items'] = array_values($menucards['items']);
$menucardsarray[] = $menucards;
}
return $menucardsarray;
}
}
@@ -0,0 +1,319 @@
<?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\helpers;
use core_reportbuilder\local\filters\boolean_select;
use core_reportbuilder\local\filters\date;
use core_reportbuilder\local\filters\number;
use core_reportbuilder\local\filters\select;
use core_reportbuilder\local\filters\text;
use core_reportbuilder\local\report\column;
use core_reportbuilder\local\report\filter;
use lang_string;
use stdClass;
use core_customfield\data_controller;
use core_customfield\field_controller;
use core_customfield\handler;
/**
* Helper class for course custom fields.
*
* @package core_reportbuilder
* @copyright 2021 Sara Arjona <sara@moodle.com> based on David Matamoros <davidmc@moodle.com> code.
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class custom_fields {
/** @var string $entityname Name of the entity */
private $entityname;
/** @var handler $handler The handler for the customfields */
private $handler;
/** @var int $tablefieldalias The table alias and the field name (table.field) that matches the customfield instanceid. */
private $tablefieldalias;
/** @var array additional joins */
private $joins = [];
/**
* Class customfields constructor.
*
* @param string $tablefieldalias table alias and the field name (table.field) that matches the customfield instanceid.
* @param string $entityname name of the entity in the report where we add custom fields.
* @param string $component component name of full frankenstyle plugin name.
* @param string $area name of the area (each component/plugin may define handlers for multiple areas).
* @param int $itemid item id if the area uses them (usually not used).
*/
public function __construct(string $tablefieldalias, string $entityname, string $component, string $area, int $itemid = 0) {
$this->tablefieldalias = $tablefieldalias;
$this->entityname = $entityname;
$this->handler = handler::get_handler($component, $area, $itemid);
}
/**
* Additional join that is needed.
*
* @param string $join
* @return self
*/
public function add_join(string $join): self {
$this->joins[trim($join)] = trim($join);
return $this;
}
/**
* Additional joins that are needed.
*
* @param array $joins
* @return self
*/
public function add_joins(array $joins): self {
foreach ($joins as $join) {
$this->add_join($join);
}
return $this;
}
/**
* Return joins
*
* @return string[]
*/
private function get_joins(): array {
return array_values($this->joins);
}
/**
* Get table alias for given custom field
*
* The entity name is used to ensure the alias differs when the entity is used multiple times within the same report, each
* having their own table alias/join
*
* @param field_controller $field
* @return string
*/
private function get_table_alias(field_controller $field): string {
static $aliases = [];
$aliaskey = "{$this->entityname}_{$field->get('id')}";
if (!array_key_exists($aliaskey, $aliases)) {
$aliases[$aliaskey] = database::generate_alias();
}
return $aliases[$aliaskey];
}
/**
* Get table join for given custom field
*
* @param field_controller $field
* @return string
*/
private function get_table_join(field_controller $field): string {
$customdatatablealias = $this->get_table_alias($field);
return "LEFT JOIN {customfield_data} {$customdatatablealias}
ON {$customdatatablealias}.fieldid = {$field->get('id')}
AND {$customdatatablealias}.instanceid = {$this->tablefieldalias}";
}
/**
* Gets the custom fields columns for the report.
*
* Column will be named as 'customfield_' + customfield shortname.
*
* @return column[]
*/
public function get_columns(): array {
global $DB;
$columns = [];
$categorieswithfields = $this->handler->get_categories_with_fields();
foreach ($categorieswithfields as $fieldcategory) {
$categoryfields = $fieldcategory->get_fields();
foreach ($categoryfields as $field) {
$customdatatablealias = $this->get_table_alias($field);
$datacontroller = data_controller::create(0, null, $field);
$datafield = $datacontroller->datafield();
$datafieldsql = "{$customdatatablealias}.{$datafield}";
// Long text fields should be cast for Oracle, for aggregation support.
$columntype = $this->get_column_type($field, $datafield);
if ($columntype === column::TYPE_LONGTEXT && $DB->get_dbfamily() === 'oracle') {
$datafieldsql = $DB->sql_order_by_text($datafieldsql, 1024);
}
// Select enough fields to re-create and format each custom field instance value.
$selectfields = "{$customdatatablealias}.id, {$customdatatablealias}.contextid";
if ($datafield === 'value') {
// We will take the format into account when displaying the individual values.
$selectfields .= ", {$customdatatablealias}.valueformat, {$customdatatablealias}.valuetrust";
}
$columns[] = (new column(
'customfield_' . $field->get('shortname'),
new lang_string('customfieldcolumn', 'core_reportbuilder', $field->get_formatted_name(false)),
$this->entityname
))
->add_joins($this->get_joins())
->add_join($this->get_table_join($field))
->add_field($datafieldsql, $datafield)
->add_fields($selectfields)
->add_field($this->tablefieldalias, 'tablefieldalias')
->set_type($columntype)
->set_is_sortable($columntype !== column::TYPE_LONGTEXT)
->add_callback(static function($value, stdClass $row, field_controller $field): string {
if ($row->tablefieldalias === null) {
return '';
}
return (string) data_controller::create(0, $row, $field)->export_value();
}, $field)
// Important. If the handler implements can_view() function, it will be called with parameter $instanceid=0.
// This means that per-instance access validation will be ignored.
->set_is_available($this->handler->can_view($field, 0));
}
}
return $columns;
}
/**
* Returns the column type
*
* @param field_controller $field
* @param string $datafield
* @return int
*/
private function get_column_type(field_controller $field, string $datafield): int {
if ($field->get('type') === 'checkbox') {
return column::TYPE_BOOLEAN;
}
if ($field->get('type') === 'date') {
return column::TYPE_TIMESTAMP;
}
if ($field->get('type') === 'select') {
return column::TYPE_TEXT;
}
if ($datafield === 'intvalue') {
return column::TYPE_INTEGER;
}
if ($datafield === 'decvalue') {
return column::TYPE_FLOAT;
}
if ($datafield === 'value') {
return column::TYPE_LONGTEXT;
}
return column::TYPE_TEXT;
}
/**
* Returns all available filters on custom fields.
*
* Filter will be named as 'customfield_' + customfield shortname.
*
* @return filter[]
*/
public function get_filters(): array {
global $DB;
$filters = [];
$categorieswithfields = $this->handler->get_categories_with_fields();
foreach ($categorieswithfields as $fieldcategory) {
$categoryfields = $fieldcategory->get_fields();
foreach ($categoryfields as $field) {
$customdatatablealias = $this->get_table_alias($field);
$datacontroller = data_controller::create(0, null, $field);
$datafield = $datacontroller->datafield();
$datafieldsql = "{$customdatatablealias}.{$datafield}";
if ($datafield === 'value') {
$datafieldsql = $DB->sql_cast_to_char($datafieldsql);
}
$typeclass = $this->get_filter_class_type($datacontroller);
$filter = (new filter(
$typeclass,
'customfield_' . $field->get('shortname'),
new lang_string('customfieldcolumn', 'core_reportbuilder', $field->get_formatted_name(false)),
$this->entityname,
$datafieldsql
))
->add_joins($this->get_joins())
->add_join($this->get_table_join($field));
// Options are stored inside configdata json string and we need to convert it to array.
if ($field->get('type') === 'select') {
$filter->set_options_callback(static function() use ($field): array {
return $field->get_options();
});
}
$filters[] = $filter;
}
}
return $filters;
}
/**
* Returns class for the filter element that should be used for the field
*
* In some situation we can assume what kind of data is stored in the customfield plugin and we can
* display appropriate filter form element. For all others assume text filter.
*
* @param data_controller $datacontroller
* @return string
*/
private function get_filter_class_type(data_controller $datacontroller): string {
$type = $datacontroller->get_field()->get('type');
switch ($type) {
case 'checkbox':
$classtype = boolean_select::class;
break;
case 'date':
$classtype = date::class;
break;
case 'select':
$classtype = select::class;
break;
default:
// To support third party field type we need to account for stored numbers.
$datafield = $datacontroller->datafield();
if ($datafield === 'intvalue' || $datafield === 'decvalue') {
$classtype = number::class;
} else {
$classtype = text::class;
}
break;
}
return $classtype;
}
}
@@ -0,0 +1,188 @@
<?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\helpers;
use coding_exception;
use core_text;
/**
* Helper functions for DB manipulations
*
* @package core_reportbuilder
* @copyright 2019 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class database {
/** @var string Prefix for generated aliases */
private const GENERATE_ALIAS_PREFIX = 'rbalias';
/** @var string Prefix for generated param names names */
private const GENERATE_PARAM_PREFIX = 'rbparam';
/**
* Generates unique table/column alias that must be used in generated SQL
*
* @param string $suffix Optional string to append to alias
* @return string
*/
public static function generate_alias(string $suffix = ''): string {
static $aliascount = 0;
return static::GENERATE_ALIAS_PREFIX . ($aliascount++) . $suffix;
}
/**
* Generate multiple unique table/column aliases, see {@see generate_alias} for info
*
* @param int $count
* @param string $suffix
* @return string[]
*/
public static function generate_aliases(int $count, string $suffix = ''): array {
return array_map([static::class, 'generate_alias'], array_fill(0, $count, $suffix));
}
/**
* Generates unique parameter name that must be used in generated SQL
*
* When passing the returned value to {@see \moodle_database::get_in_or_equal} it's recommended to define the suffix
*
* @param string $suffix Optional string to append to parameter name
* @return string
*/
public static function generate_param_name(string $suffix = ''): string {
static $paramcount = 0;
return static::GENERATE_PARAM_PREFIX . ($paramcount++) . $suffix;
}
/**
* Generate multiple unique parameter names, see {@see generate_param_name} for info
*
* @param int $count
* @param string $suffix
* @return string[]
*/
public static function generate_param_names(int $count, string $suffix = ''): array {
return array_map([static::class, 'generate_param_name'], array_fill(0, $count, $suffix));
}
/**
* Validate that parameter names were generated using {@see generate_param_name}.
*
* @param array $params
* @return bool
* @throws coding_exception For invalid params.
*/
public static function validate_params(array $params): bool {
$nonmatchingkeys = array_filter($params, static function($key): bool {
return !preg_match('/^' . static::GENERATE_PARAM_PREFIX . '[\d]+/', $key);
}, ARRAY_FILTER_USE_KEY);
if (!empty($nonmatchingkeys)) {
throw new coding_exception('Invalid parameter names', implode(', ', array_keys($nonmatchingkeys)));
}
return true;
}
/**
* Replace parameter names within given SQL expression, allowing caller to specify callback to handle their replacement
* primarily to ensure uniqueness when the expression is to be used as part of a larger query
*
* @param string $sql
* @param array $params Parameter names
* @param callable $callback Method that takes a single string parameter, and returns another string
* @return string
*/
public static function sql_replace_parameter_names(string $sql, array $params, callable $callback): string {
foreach ($params as $param) {
// Pattern to look for param within the SQL.
$pattern = '/:(?<param>' . preg_quote($param) . ')\b/';
$sql = preg_replace_callback($pattern, function(array $matches) use ($callback): string {
return ':' . $callback($matches['param']);
}, $sql);
}
return $sql;
}
/**
* Replace parameter names within given SQL expression, returning updated SQL and parameter elements
*
* {@see sql_replace_parameter_names}
*
* @param string $sql
* @param array $params Parameter name/values
* @param callable $callback
* @return array [$sql, $params]
*/
public static function sql_replace_parameters(string $sql, array $params, callable $callback): array {
$transformedsql = static::sql_replace_parameter_names($sql, array_keys($params), $callback);
$transformedparams = [];
foreach ($params as $name => $value) {
$transformedparams[$callback($name)] = $value;
}
return [$transformedsql, $transformedparams];
}
/**
* Generate SQL expression for sorting group concatenated fields
*
* @param string $field The original field or SQL expression
* @param string|null $sort A valid SQL ORDER BY to sort the concatenated fields, if omitted then $field will be used
* @return string
*/
public static function sql_group_concat_sort(string $field, string $sort = null): string {
global $DB;
// Fallback to sorting by the specified field, unless it contains parameters which would be duplicated.
if ($sort === null && !preg_match('/[:?$]/', $field)) {
$fieldsort = $field;
} else {
$fieldsort = $sort;
}
// Nothing to sort by.
if ($fieldsort === null) {
return '';
}
// If the sort specifies a direction, we need to handle that differently in Postgres.
if ($DB->get_dbfamily() === 'postgres') {
$fieldsortdirection = '';
preg_match('/(?<direction>ASC|DESC)?$/i', $fieldsort, $matches);
if (array_key_exists('direction', $matches)) {
$fieldsortdirection = $matches['direction'];
$fieldsort = core_text::substr($fieldsort, 0, -(core_text::strlen($fieldsortdirection)));
}
// Cast sort, stick the direction on the end.
$fieldsort = $DB->sql_cast_to_char($fieldsort) . ' ' . $fieldsortdirection;
}
return $fieldsort;
}
}
@@ -0,0 +1,69 @@
<?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\helpers;
use stdClass;
/**
* Class containing helper methods for formatting column data via callbacks
*
* @package core_reportbuilder
* @copyright 2021 Sara Arjona <sara@moodle.com> based on Alberto Lara Hernández <albertolara@moodle.com> code.
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class format {
/**
* Returns formatted date.
*
* @param int|null $value Unix timestamp
* @param stdClass $row
* @param string|null $format Format string for strftime
* @return string
*/
public static function userdate(?int $value, stdClass $row, ?string $format = null): string {
return $value ? userdate($value, $format) : '';
}
/**
* Returns yes/no string depending on the given value
*
* @param bool|null $value
* @return string
*/
public static function boolean_as_text(?bool $value): string {
if ($value === null) {
return '';
}
return $value ? get_string('yes') : get_string('no');
}
/**
* Returns float value as a percentage
*
* @param float|null $value
* @return string
*/
public static function percent(?float $value): string {
if ($value === null) {
return '';
}
return get_string('percents', 'moodle', format_float($value));
}
}
@@ -0,0 +1,480 @@
<?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\helpers;
use stdClass;
use invalid_parameter_exception;
use core\persistent;
use core_reportbuilder\datasource;
use core_reportbuilder\manager;
use core_reportbuilder\local\models\column;
use core_reportbuilder\local\models\filter;
use core_reportbuilder\local\models\report as report_model;
use core_tag_tag;
/**
* Helper class for manipulating custom reports and their elements (columns, filters, conditions, etc)
*
* @package core_reportbuilder
* @copyright 2021 Paul Holden <paulh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class report {
/**
* Create custom report
*
* @param stdClass $data
* @param bool $default If $default is set to true it will populate report with default layout as defined by the selected
* source. These include pre-defined columns, filters and conditions.
* @return report_model
*/
public static function create_report(stdClass $data, bool $default = true): report_model {
$data->name = trim($data->name);
$data->type = datasource::TYPE_CUSTOM_REPORT;
// Create report persistent.
$report = manager::create_report_persistent($data);
// Add datasource default columns, filters and conditions to the report.
if ($default) {
$source = $report->get('source');
/** @var datasource $datasource */
$datasource = new $source($report);
$datasource->add_default_columns();
$datasource->add_default_filters();
$datasource->add_default_conditions();
}
// Report tags.
if (property_exists($data, "tags")) {
core_tag_tag::set_item_tags('core_reportbuilder', 'reportbuilder_report', $report->get('id'),
$report->get_context(), $data->tags);
}
return $report;
}
/**
* Update custom report
*
* @param stdClass $data
* @return report_model
*/
public static function update_report(stdClass $data): report_model {
$report = report_model::get_record(['id' => $data->id, 'type' => datasource::TYPE_CUSTOM_REPORT]);
if ($report === false) {
throw new invalid_parameter_exception('Invalid report');
}
$report->set_many([
'name' => trim($data->name),
'uniquerows' => $data->uniquerows,
])->update();
// Report tags.
if (property_exists($data, "tags")) {
core_tag_tag::set_item_tags('core_reportbuilder', 'reportbuilder_report', $report->get('id'),
$report->get_context(), $data->tags);
}
return $report;
}
/**
* Delete custom report
*
* @param int $reportid
* @return bool
* @throws invalid_parameter_exception
*/
public static function delete_report(int $reportid): bool {
$report = report_model::get_record(['id' => $reportid, 'type' => datasource::TYPE_CUSTOM_REPORT]);
if ($report === false) {
throw new invalid_parameter_exception('Invalid report');
}
// Report tags.
core_tag_tag::remove_all_item_tags('core_reportbuilder', 'reportbuilder_report', $report->get('id'));
return $report->delete();
}
/**
* Add given column to report
*
* @param int $reportid
* @param string $uniqueidentifier
* @return column
* @throws invalid_parameter_exception
*/
public static function add_report_column(int $reportid, string $uniqueidentifier): column {
$report = manager::get_report_from_id($reportid);
// Ensure column is available.
if (!array_key_exists($uniqueidentifier, $report->get_columns())) {
throw new invalid_parameter_exception('Invalid column');
}
$column = new column(0, (object) [
'reportid' => $reportid,
'uniqueidentifier' => $uniqueidentifier,
'columnorder' => column::get_max_columnorder($reportid, 'columnorder') + 1,
'sortorder' => column::get_max_columnorder($reportid, 'sortorder') + 1,
]);
return $column->create();
}
/**
* Delete given column from report
*
* @param int $reportid
* @param int $columnid
* @return bool
* @throws invalid_parameter_exception
*/
public static function delete_report_column(int $reportid, int $columnid): bool {
global $DB;
$column = column::get_record(['id' => $columnid, 'reportid' => $reportid]);
if ($column === false) {
throw new invalid_parameter_exception('Invalid column');
}
// After deletion, re-index remaining report columns.
if ($result = $column->delete()) {
$sqlupdateorder = '
UPDATE {' . column::TABLE . '}
SET columnorder = columnorder - 1
WHERE reportid = :reportid
AND columnorder > :columnorder';
$DB->execute($sqlupdateorder, ['reportid' => $reportid, 'columnorder' => $column->get('columnorder')]);
}
return $result;
}
/**
* Re-order given column within a report
*
* @param int $reportid
* @param int $columnid
* @param int $position
* @return bool
* @throws invalid_parameter_exception
*/
public static function reorder_report_column(int $reportid, int $columnid, int $position): bool {
$column = column::get_record(['id' => $columnid, 'reportid' => $reportid]);
if ($column === false) {
throw new invalid_parameter_exception('Invalid column');
}
// Get the rest of the report columns, excluding the one we are moving.
$columns = column::get_records_select('reportid = :reportid AND id <> :id', [
'reportid' => $reportid,
'id' => $columnid,
], 'columnorder');
return static::reorder_persistents_by_field($column, $columns, $position, 'columnorder');
}
/**
* Re-order given column sorting within a report
*
* @param int $reportid
* @param int $columnid
* @param int $position
* @return bool
* @throws invalid_parameter_exception
*/
public static function reorder_report_column_sorting(int $reportid, int $columnid, int $position): bool {
$column = column::get_record(['id' => $columnid, 'reportid' => $reportid]);
if ($column === false) {
throw new invalid_parameter_exception('Invalid column');
}
// Get the rest of the report columns, excluding the one we are moving.
$columns = column::get_records_select('reportid = :reportid AND id <> :id', [
'reportid' => $reportid,
'id' => $columnid,
], 'sortorder');
return static::reorder_persistents_by_field($column, $columns, $position, 'sortorder');
}
/**
* Toggle sorting options for given column within a report
*
* @param int $reportid
* @param int $columnid
* @param bool $enabled
* @param int $direction
* @return bool
* @throws invalid_parameter_exception
*/
public static function toggle_report_column_sorting(int $reportid, int $columnid, bool $enabled,
int $direction = SORT_ASC): bool {
$column = column::get_record(['id' => $columnid, 'reportid' => $reportid]);
if ($column === false) {
throw new invalid_parameter_exception('Invalid column');
}
return $column->set_many([
'sortenabled' => $enabled,
'sortdirection' => $direction,
])->update();
}
/**
* Add given condition to report
*
* @param int $reportid
* @param string $uniqueidentifier
* @return filter
* @throws invalid_parameter_exception
*/
public static function add_report_condition(int $reportid, string $uniqueidentifier): filter {
$report = manager::get_report_from_id($reportid);
// Ensure condition is available.
if (!array_key_exists($uniqueidentifier, $report->get_conditions())) {
throw new invalid_parameter_exception('Invalid condition');
}
// Ensure condition wasn't already added.
if (array_key_exists($uniqueidentifier, $report->get_active_conditions())) {
throw new invalid_parameter_exception('Duplicate condition');
}
$condition = new filter(0, (object) [
'reportid' => $reportid,
'uniqueidentifier' => $uniqueidentifier,
'iscondition' => true,
'filterorder' => filter::get_max_filterorder($reportid, true) + 1,
]);
return $condition->create();
}
/**
* Delete given condition from report
*
* @param int $reportid
* @param int $conditionid
* @return bool
* @throws invalid_parameter_exception
*/
public static function delete_report_condition(int $reportid, int $conditionid): bool {
global $DB;
$condition = filter::get_condition_record($reportid, $conditionid);
if ($condition === false) {
throw new invalid_parameter_exception('Invalid condition');
}
// After deletion, re-index remaining report conditions.
if ($result = $condition->delete()) {
$sqlupdateorder = '
UPDATE {' . filter::TABLE . '}
SET filterorder = filterorder - 1
WHERE reportid = :reportid
AND filterorder > :filterorder
AND iscondition = 1';
$DB->execute($sqlupdateorder, ['reportid' => $reportid, 'filterorder' => $condition->get('filterorder')]);
}
return $result;
}
/**
* Re-order given condition within a report
*
* @param int $reportid
* @param int $conditionid
* @param int $position
* @return bool
* @throws invalid_parameter_exception
*/
public static function reorder_report_condition(int $reportid, int $conditionid, int $position): bool {
$condition = filter::get_condition_record($reportid, $conditionid);
if ($condition === false) {
throw new invalid_parameter_exception('Invalid condition');
}
// Get the rest of the report conditions, excluding the one we are moving.
$conditions = filter::get_records_select('reportid = :reportid AND iscondition = 1 AND id <> :id', [
'reportid' => $reportid,
'id' => $conditionid,
], 'filterorder');
return static::reorder_persistents_by_field($condition, $conditions, $position, 'filterorder');
}
/**
* Add given filter to report
*
* @param int $reportid
* @param string $uniqueidentifier
* @return filter
* @throws invalid_parameter_exception
*/
public static function add_report_filter(int $reportid, string $uniqueidentifier): filter {
$report = manager::get_report_from_id($reportid);
// Ensure filter is available.
if (!array_key_exists($uniqueidentifier, $report->get_filters())) {
throw new invalid_parameter_exception('Invalid filter');
}
// Ensure filter wasn't already added.
if (array_key_exists($uniqueidentifier, $report->get_active_filters())) {
throw new invalid_parameter_exception('Duplicate filter');
}
$filter = new filter(0, (object) [
'reportid' => $reportid,
'uniqueidentifier' => $uniqueidentifier,
'filterorder' => filter::get_max_filterorder($reportid) + 1,
]);
return $filter->create();
}
/**
* Delete given filter from report
*
* @param int $reportid
* @param int $filterid
* @return bool
* @throws invalid_parameter_exception
*/
public static function delete_report_filter(int $reportid, int $filterid): bool {
global $DB;
$filter = filter::get_filter_record($reportid, $filterid);
if ($filter === false) {
throw new invalid_parameter_exception('Invalid filter');
}
// After deletion, re-index remaining report filters.
if ($result = $filter->delete()) {
$sqlupdateorder = '
UPDATE {' . filter::TABLE . '}
SET filterorder = filterorder - 1
WHERE reportid = :reportid
AND filterorder > :filterorder
AND iscondition = 0';
$DB->execute($sqlupdateorder, ['reportid' => $reportid, 'filterorder' => $filter->get('filterorder')]);
}
return $result;
}
/**
* Re-order given filter within a report
*
* @param int $reportid
* @param int $filterid
* @param int $position
* @return bool
* @throws invalid_parameter_exception
*/
public static function reorder_report_filter(int $reportid, int $filterid, int $position): bool {
$filter = filter::get_filter_record($reportid, $filterid);
if ($filter === false) {
throw new invalid_parameter_exception('Invalid filter');
}
// Get the rest of the report filters, excluding the one we are moving.
$filters = filter::get_records_select('reportid = :reportid AND iscondition = 0 AND id <> :id', [
'reportid' => $reportid,
'id' => $filterid,
], 'filterorder');
return static::reorder_persistents_by_field($filter, $filters, $position, 'filterorder');
}
/**
* Get available columns for a given report
*
* @param report_model $persistent
* @return array
*
* @deprecated since Moodle 4.1 - please do not use this function any more, {@see custom_report_column_cards_exporter}
*/
public static function get_available_columns(report_model $persistent): array {
debugging('The function ' . __FUNCTION__ . '() is deprecated, please do not use it any more. ' .
'See \'custom_report_column_cards_exporter\' class for replacement', DEBUG_DEVELOPER);
$available = [];
$report = manager::get_report_from_persistent($persistent);
// Get current report columns.
foreach ($report->get_columns() as $column) {
$entityname = $column->get_entity_name();
$entitytitle = $column->get_title();
if (!array_key_exists($entityname, $available)) {
$available[$entityname] = [
'name' => (string) $report->get_entity_title($entityname),
'key' => $entityname,
'items' => [],
];
}
$available[$entityname]['items'][] = [
'name' => $entitytitle,
'identifier' => $column->get_unique_identifier(),
'title' => get_string('addcolumn', 'core_reportbuilder', $entitytitle),
'action' => 'report-add-column'
];
}
return array_values($available);
}
/**
* Helper method for re-ordering given persistents (columns, filters, etc)
*
* @param persistent $persistent The persistent we are moving
* @param persistent[] $persistents The rest of the persistents
* @param int $position
* @param string $field The field we need to update
* @return bool
*/
private static function reorder_persistents_by_field(persistent $persistent, array $persistents, int $position,
string $field): bool {
// Splice into new position.
array_splice($persistents, $position - 1, 0, [$persistent]);
$fieldorder = 1;
foreach ($persistents as $persistent) {
$persistent->set($field, $fieldorder++)
->update();
}
return true;
}
}
@@ -0,0 +1,398 @@
<?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\helpers;
use context_user;
use core_user;
use invalid_parameter_exception;
use stdClass;
use stored_file;
use table_dataformat_export_format;
use core\message\message;
use core\plugininfo\dataformat;
use core_reportbuilder\local\models\audience as audience_model;
use core_reportbuilder\local\models\schedule as model;
use core_reportbuilder\table\custom_report_table_view;
/**
* Helper class for report schedule related methods
*
* @package core_reportbuilder
* @copyright 2021 Paul Holden <paulh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class schedule {
/**
* Create report schedule, calculate when it should be next sent
*
* @param stdClass $data
* @param int|null $timenow Time to use as comparison against current date (defaults to current time)
* @return model
*/
public static function create_schedule(stdClass $data, ?int $timenow = null): model {
$data->name = trim($data->name);
$schedule = (new model(0, $data));
$schedule->set('timenextsend', self::calculate_next_send_time($schedule, $timenow));
return $schedule->create();
}
/**
* Update report schedule
*
* @param stdClass $data
* @return model
* @throws invalid_parameter_exception
*/
public static function update_schedule(stdClass $data): model {
$schedule = model::get_record(['id' => $data->id, 'reportid' => $data->reportid]);
if ($schedule === false) {
throw new invalid_parameter_exception('Invalid schedule');
}
// Normalize model properties.
$data = array_intersect_key((array) $data, model::properties_definition());
if (array_key_exists('name', $data)) {
$data['name'] = trim($data['name']);
}
$schedule->set_many($data);
$schedule->set('timenextsend', self::calculate_next_send_time($schedule))
->update();
return $schedule;
}
/**
* Toggle report schedule enabled
*
* @param int $reportid
* @param int $scheduleid
* @param bool $enabled
* @return bool
* @throws invalid_parameter_exception
*/
public static function toggle_schedule(int $reportid, int $scheduleid, bool $enabled): bool {
$schedule = model::get_record(['id' => $scheduleid, 'reportid' => $reportid]);
if ($schedule === false) {
throw new invalid_parameter_exception('Invalid schedule');
}
return $schedule->set('enabled', $enabled)->update();
}
/**
* Return array of users who match the audience records added to the given schedule
*
* @param model $schedule
* @return stdClass[]
*/
public static function get_schedule_report_users(model $schedule): array {
global $DB;
$audienceids = (array) json_decode($schedule->get('audiences'));
// Retrieve all selected audience records for the schedule.
[$audienceselect, $audienceparams] = $DB->get_in_or_equal($audienceids, SQL_PARAMS_NAMED, 'aid', true, null);
$audiences = audience_model::get_records_select("id {$audienceselect}", $audienceparams);
// Now convert audiences to SQL for user retrieval.
[$wheres, $params] = audience::user_audience_sql($audiences);
if (count($wheres) === 0) {
return [];
}
[$userorder] = users_order_by_sql('u');
$sql = 'SELECT u.*
FROM {user} u
WHERE (' . implode(' OR ', $wheres) . ')
AND u.deleted = 0
ORDER BY ' . $userorder;
return $DB->get_records_sql($sql, $params);
}
/**
* Return count of schedule report rows
*
* @param model $schedule
* @return int
*/
public static function get_schedule_report_count(model $schedule): int {
global $DB;
$table = custom_report_table_view::create($schedule->get('reportid'));
$table->setup();
return $DB->count_records_sql($table->countsql, $table->countparams);
}
/**
* Generate stored file instance for given schedule, in user draft
*
* @param model $schedule
* @return stored_file
*/
public static function get_schedule_report_file(model $schedule): stored_file {
global $CFG, $USER;
require_once("{$CFG->libdir}/filelib.php");
$table = custom_report_table_view::create($schedule->get('reportid'));
$table->setup();
$table->query_db(0, false);
// Set up table as if it were being downloaded, retrieve appropriate export class (ensure output buffer is
// cleaned in order to instantiate export class without exception).
ob_start();
$table->download = $schedule->get('format');
$exportclass = new table_dataformat_export_format($table, $table->download);
ob_end_clean();
// Create our schedule report stored file temporarily in user draft.
$filerecord = [
'contextid' => context_user::instance($USER->id)->id,
'component' => 'user',
'filearea' => 'draft',
'itemid' => file_get_unused_draft_itemid(),
'filepath' => '/',
'filename' => clean_filename($schedule->get_formatted_name()),
];
$storedfile = \core\dataformat::write_data_to_filearea(
$filerecord,
$table->download,
$exportclass->format_data($table->headers),
$table->rawdata,
static function(stdClass $record, bool $supportshtml) use ($table, $exportclass): array {
$record = $table->format_row($record);
if (!$supportshtml) {
$record = $exportclass->format_data($record);
}
return $record;
}
);
$table->close_recordset();
return $storedfile;
}
/**
* Check whether given schedule needs to be sent
*
* @param model $schedule
* @return bool
*/
public static function should_send_schedule(model $schedule): bool {
if (!$schedule->get('enabled')) {
return false;
}
$timenow = time();
// Ensure we've reached the initial scheduled start time.
$timescheduled = $schedule->get('timescheduled');
if ($timescheduled > $timenow) {
return false;
}
// If there's no recurrence, check whether it's been sent since initial scheduled start time. This ensures that even if
// the schedule was manually sent beforehand, it'll still be automatically sent once the start time is first reached.
if ($schedule->get('recurrence') === model::RECURRENCE_NONE) {
return $schedule->get('timelastsent') < $timescheduled;
}
return $schedule->get('timenextsend') <= $timenow;
}
/**
* Calculate the next time a schedule should be sent, based on it's recurrence and when it was initially scheduled. Ensures
* returned value is after the current date
*
* @param model $schedule
* @param int|null $timenow Time to use as comparison against current date (defaults to current time)
* @return int
*/
public static function calculate_next_send_time(model $schedule, ?int $timenow = null): int {
global $CFG;
$timenow = $timenow ?? time();
$recurrence = $schedule->get('recurrence');
$timescheduled = $schedule->get('timescheduled');
// If no recurrence is set or we haven't reached last sent date, return early.
if ($recurrence === model::RECURRENCE_NONE || $timescheduled > $timenow) {
return $timescheduled;
}
// Extract attributes from date (year, month, day, hours, minutes).
[
'year' => $year,
'mon' => $month,
'mday' => $day,
'wday' => $dayofweek,
'hours' => $hour,
'minutes' => $minute,
] = usergetdate($timescheduled, $CFG->timezone);
switch ($recurrence) {
case model::RECURRENCE_DAILY:
$day += 1;
break;
case model::RECURRENCE_WEEKDAYS:
$day += 1;
$calendar = \core_calendar\type_factory::get_calendar_instance();
$weekend = get_config('core', 'calendar_weekend');
// Increment day until day of week falls on a weekday.
while ((bool) ($weekend & (1 << (++$dayofweek % $calendar->get_num_weekdays())))) {
$day++;
}
break;
case model::RECURRENCE_WEEKLY:
$day += 7;
break;
case model::RECURRENCE_MONTHLY:
$month += 1;
break;
case model::RECURRENCE_ANNUALLY:
$year += 1;
break;
}
// We need to recursively increment the timestamp until we get one after the current time.
$timestamp = make_timestamp($year, $month, $day, $hour, $minute, 0, $CFG->timezone);
if ($timestamp < $timenow) {
// Ensure we don't modify anything in the original model.
$scheduleclone = new model(0, $schedule->to_record());
return self::calculate_next_send_time(
$scheduleclone->set('timescheduled', $timestamp), $timenow);
} else {
return $timestamp;
}
}
/**
* Send schedule message to user
*
* @param model $schedule
* @param stdClass $user
* @param stored_file $attachment
* @return bool
*/
public static function send_schedule_message(model $schedule, stdClass $user, stored_file $attachment): bool {
$message = new message();
$message->component = 'moodle';
$message->name = 'reportbuilderschedule';
$message->courseid = SITEID;
$message->userfrom = core_user::get_noreply_user();
$message->userto = $user;
$message->subject = $schedule->get('subject');
$message->fullmessage = $schedule->get('message');
$message->fullmessageformat = $schedule->get('messageformat');
$message->fullmessagehtml = $message->fullmessage;
$message->smallmessage = $message->fullmessage;
// Attach report to outgoing message.
$message->attachment = $attachment;
$message->attachname = $attachment->get_filename();
return (bool) message_send($message);
}
/**
* Delete report schedule
*
* @param int $reportid
* @param int $scheduleid
* @return bool
* @throws invalid_parameter_exception
*/
public static function delete_schedule(int $reportid, int $scheduleid): bool {
$schedule = model::get_record(['id' => $scheduleid, 'reportid' => $reportid]);
if ($schedule === false) {
throw new invalid_parameter_exception('Invalid schedule');
}
return $schedule->delete();
}
/**
* Return list of available data formats
*
* @return string[]
*/
public static function get_format_options(): array {
$dataformats = dataformat::get_enabled_plugins();
return array_map(static function(string $pluginname): string {
return get_string('dataformat', 'dataformat_' . $pluginname);
}, $dataformats);
}
/**
* Return list of available view as user options
*
* @return string[]
*/
public static function get_viewas_options(): array {
return [
model::REPORT_VIEWAS_CREATOR => get_string('scheduleviewascreator', 'core_reportbuilder'),
model::REPORT_VIEWAS_RECIPIENT => get_string('scheduleviewasrecipient', 'core_reportbuilder'),
model::REPORT_VIEWAS_USER => get_string('userselect', 'core_reportbuilder'),
];
}
/**
* Return list of recurrence options
*
* @return string[]
*/
public static function get_recurrence_options(): array {
return [
model::RECURRENCE_NONE => get_string('none'),
model::RECURRENCE_DAILY => get_string('recurrencedaily', 'core_reportbuilder'),
model::RECURRENCE_WEEKDAYS => get_string('recurrenceweekdays', 'core_reportbuilder'),
model::RECURRENCE_WEEKLY => get_string('recurrenceweekly', 'core_reportbuilder'),
model::RECURRENCE_MONTHLY => get_string('recurrencemonthly', 'core_reportbuilder'),
model::RECURRENCE_ANNUALLY => get_string('recurrenceannually', 'core_reportbuilder'),
];
}
/**
* Return list of options for when report is empty
*
* @return string[]
*/
public static function get_report_empty_options(): array {
return [
model::REPORT_EMPTY_SEND_EMPTY => get_string('scheduleemptysendwithattachment', 'core_reportbuilder'),
model::REPORT_EMPTY_SEND_WITHOUT => get_string('scheduleemptysendwithoutattachment', 'core_reportbuilder'),
model::REPORT_EMPTY_DONT_SEND => get_string('scheduleemptydontsend', 'core_reportbuilder'),
];
}
}
@@ -0,0 +1,175 @@
<?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\helpers;
use core_text;
/**
* This class handles the setting and retrieving of a users' filter values for given reports
*
* It is currently using the user preference API as a storage mechanism
*
* @package core_reportbuilder
* @copyright 2021 Paul Holden <paulh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class user_filter_manager {
/** @var int The size of each chunk, matching the maximum length of a single user preference */
private const PREFERENCE_CHUNK_SIZE = 1333;
/** @var string The prefix used to name the stored user preferences */
private const PREFERENCE_NAME_PREFIX = 'reportbuilder-report-';
/**
* Generate user preference name for given report
*
* @param int $reportid
* @param int $index
* @return string
*/
private static function user_preference_name(int $reportid, int $index): string {
return static::PREFERENCE_NAME_PREFIX . "{$reportid}-{$index}";
}
/**
* Set user filters for given report
*
* @param int $reportid
* @param array $values
* @param int|null $userid
* @return bool
*/
public static function set(int $reportid, array $values, int $userid = null): bool {
$jsonvalues = json_encode($values);
$jsonchunks = str_split($jsonvalues, static::PREFERENCE_CHUNK_SIZE);
foreach ($jsonchunks as $index => $jsonchunk) {
$userpreference = static::user_preference_name($reportid, $index);
set_user_preference($userpreference, $jsonchunk, $userid);
}
// Ensure any subsequent preferences are reset (to account for number of chunks decreasing).
static::reset_all($reportid, $userid, $index + 1);
return true;
}
/**
* Get user filters for given report
*
* @param int $reportid
* @param int|null $userid
* @return array
*/
public static function get(int $reportid, int $userid = null): array {
$jsonvalues = '';
$index = 0;
// We'll repeatedly append chunks to our JSON string, until we hit one that is below the maximum length.
do {
$userpreference = static::user_preference_name($reportid, $index++);
$jsonchunk = get_user_preferences($userpreference, '', $userid);
$jsonvalues .= $jsonchunk;
} while (core_text::strlen($jsonchunk) === static::PREFERENCE_CHUNK_SIZE);
return (array) json_decode($jsonvalues);
}
/**
* Merge individual user filter values for given report
*
* @param int $reportid
* @param array $values
* @param int|null $userid
* @return bool
*/
public static function merge(int $reportid, array $values, int $userid = null): bool {
$existing = static::get($reportid, $userid);
return static::set($reportid, array_merge($existing, $values), $userid);
}
/**
* Reset all user filters for given report
*
* @param int $reportid
* @param int|null $userid
* @param int $index If specified, then preferences will be reset starting from this index
* @return bool
*/
public static function reset_all(int $reportid, int $userid = null, int $index = 0): bool {
// We'll repeatedly retrieve and reset preferences, until we hit one that is below the maximum length.
do {
$userpreference = static::user_preference_name($reportid, $index++);
$jsonchunk = get_user_preferences($userpreference, '', $userid);
unset_user_preference($userpreference, $userid);
} while (core_text::strlen($jsonchunk) === static::PREFERENCE_CHUNK_SIZE);
return true;
}
/**
* Reset single user filter for given report
*
* @param int $reportid
* @param string $uniqueidentifier
* @param int|null $userid
* @return bool
*/
public static function reset_single(int $reportid, string $uniqueidentifier, int $userid = null): bool {
$originalvalues = static::get($reportid, $userid);
// Remove any filters whose name is prefixed by given identifier.
$values = array_filter($originalvalues, static function(string $filterkey) use ($uniqueidentifier): bool {
return core_text::strpos($filterkey, $uniqueidentifier) !== 0;
}, ARRAY_FILTER_USE_KEY);
return static::set($reportid, $values, $userid);
}
/**
* Get all report filters for given user
*
* This is primarily designed for the privacy provider, and allows us to preserve all the preference logic within this class.
*
* @param int $userid
* @return array
*/
public static function get_all_for_user(int $userid): array {
global $DB;
$prefs = [];
// We need to locate the first preference chunk of all report filters.
$select = 'userid = :userid AND ' . $DB->sql_like('name', ':namelike');
$params = [
'userid' => $userid,
'namelike' => $DB->sql_like_escape(static::PREFERENCE_NAME_PREFIX) . '%-0',
];
$preferences = $DB->get_fieldset_select('user_preferences', 'name', $select, $params);
// Retrieve all found filters.
foreach ($preferences as $preference) {
preg_match('/^' . static::PREFERENCE_NAME_PREFIX . '(?<reportid>\d+)\-/', $preference, $matches);
$prefs[static::PREFERENCE_NAME_PREFIX . $matches['reportid']] = static::get((int) $matches['reportid'], $userid);
}
return $prefs;
}
}
@@ -0,0 +1,281 @@
<?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\helpers;
use core_text;
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\report\column;
use core_reportbuilder\local\report\filter;
use lang_string;
use profile_field_base;
use stdClass;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot.'/user/profile/lib.php');
/**
* Helper class for user profile fields.
*
* @package core_reportbuilder
* @copyright 2021 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class user_profile_fields {
/** @var array user profile fields */
private $userprofilefields;
/** @var string $entityname Name of the entity */
private $entityname;
/** @var int $usertablefieldalias The user table/field alias */
private $usertablefieldalias;
/** @var array additional joins */
private $joins = [];
/**
* Class userprofilefields constructor.
*
* @param string $usertablefieldalias The user table/field alias used when adding columns and filters.
* @param string $entityname The entity name used when adding columns and filters.
*/
public function __construct(string $usertablefieldalias, string $entityname) {
$this->usertablefieldalias = $usertablefieldalias;
$this->entityname = $entityname;
$this->userprofilefields = $this->get_user_profile_fields();
}
/**
* Retrieves the list of available/visible user profile fields
*
* @return profile_field_base[]
*/
private function get_user_profile_fields(): array {
return array_filter(profile_get_user_fields_with_data(0), static function(profile_field_base $profilefield): bool {
return $profilefield->is_visible();
});
}
/**
* Additional join that is needed.
*
* @param string $join
* @return self
*/
public function add_join(string $join): self {
$this->joins[trim($join)] = trim($join);
return $this;
}
/**
* Additional joins that are needed.
*
* @param array $joins
* @return self
*/
public function add_joins(array $joins): self {
foreach ($joins as $join) {
$this->add_join($join);
}
return $this;
}
/**
* Return joins
*
* @return string[]
*/
private function get_joins(): array {
return array_values($this->joins);
}
/**
* Get table alias for given profile field
*
* The entity name is used to ensure the alias differs when the entity is used multiple times within the same report, each
* having their own table alias/join
*
* @param profile_field_base $profilefield
* @return string
*/
private function get_table_alias(profile_field_base $profilefield): string {
static $aliases = [];
$aliaskey = "{$this->entityname}_{$profilefield->fieldid}";
if (!array_key_exists($aliaskey, $aliases)) {
$aliases[$aliaskey] = database::generate_alias();
}
return $aliases[$aliaskey];
}
/**
* Get table join for given profile field
*
* @param profile_field_base $profilefield
* @return string
*/
private function get_table_join(profile_field_base $profilefield): string {
$userinfotablealias = $this->get_table_alias($profilefield);
return "LEFT JOIN {user_info_data} {$userinfotablealias}
ON {$userinfotablealias}.userid = {$this->usertablefieldalias}
AND {$userinfotablealias}.fieldid = {$profilefield->fieldid}";
}
/**
* Return the user profile fields visible columns.
*
* @return column[]
*/
public function get_columns(): array {
global $DB;
$columns = [];
foreach ($this->userprofilefields as $profilefield) {
$columntype = $this->get_user_field_type($profilefield->field->datatype);
$columnfieldsql = $this->get_table_alias($profilefield) . '.data';
// Numeric (checkbox/time) fields should be cast, as should all fields for Oracle, for aggregation support.
if ($columntype === column::TYPE_BOOLEAN || $columntype === column::TYPE_TIMESTAMP) {
$columnfieldsql = "CASE WHEN {$columnfieldsql} IS NULL THEN NULL ELSE " .
$DB->sql_cast_char2int($columnfieldsql, true) . " END";
} else if ($DB->get_dbfamily() === 'oracle') {
$columnfieldsql = $DB->sql_order_by_text($columnfieldsql, 1024);
}
$columns[] = (new column(
'profilefield_' . core_text::strtolower($profilefield->field->shortname),
new lang_string('customfieldcolumn', 'core_reportbuilder', $profilefield->display_name(false)),
$this->entityname
))
->add_joins($this->get_joins())
->add_join($this->get_table_join($profilefield))
->add_field($columnfieldsql, 'data')
->set_type($columntype)
->set_is_sortable($columntype !== column::TYPE_LONGTEXT)
->add_callback(static function($value, stdClass $row, profile_field_base $field): string {
if ($value === null) {
return '';
}
$field->data = $value;
return (string) $field->display_data();
}, $profilefield);
}
return $columns;
}
/**
* Get custom user profile fields filters.
*
* @return filter[]
*/
public function get_filters(): array {
global $DB;
$filters = [];
foreach ($this->userprofilefields as $profilefield) {
$field = $this->get_table_alias($profilefield) . '.data';
$params = [];
switch ($profilefield->field->datatype) {
case 'checkbox':
$classname = boolean_select::class;
$fieldsql = "COALESCE(" . $DB->sql_cast_char2int($field, true) . ", 0)";
break;
case 'datetime':
$classname = date::class;
$fieldsql = $DB->sql_cast_char2int($field, true);
break;
case 'menu':
$classname = select::class;
$emptyparam = database::generate_param_name();
$fieldsql = "COALESCE(" . $DB->sql_compare_text($field, 255) . ", :{$emptyparam})";
$params[$emptyparam] = '';
break;
case 'text':
case 'textarea':
default:
$classname = text::class;
$emptyparam = database::generate_param_name();
$fieldsql = "COALESCE(" . $DB->sql_compare_text($field, 255) . ", :{$emptyparam})";
$params[$emptyparam] = '';
break;
}
$filter = (new filter(
$classname,
'profilefield_' . core_text::strtolower($profilefield->field->shortname),
new lang_string('customfieldcolumn', 'core_reportbuilder', $profilefield->display_name(false)),
$this->entityname,
$fieldsql,
$params
))
->add_joins($this->get_joins())
->add_join($this->get_table_join($profilefield));
// If menu type then set filter options as appropriate.
if ($profilefield->field->datatype === 'menu') {
$filter->set_options($profilefield->options);
}
$filters[] = $filter;
}
return $filters;
}
/**
* Get user profile field type for report.
*
* @param string $userfield user field.
* @return int the constant equivalent to this custom field type.
*/
protected function get_user_field_type(string $userfield): int {
switch ($userfield) {
case 'checkbox':
$customfieldtype = column::TYPE_BOOLEAN;
break;
case 'datetime':
$customfieldtype = column::TYPE_TIMESTAMP;
break;
case 'textarea':
$customfieldtype = column::TYPE_LONGTEXT;
break;
case 'menu':
case 'text':
default:
$customfieldtype = column::TYPE_TEXT;
break;
}
return $customfieldtype;
}
}