| 1 | efrain | 1 | <?php
 | 
        
           |  |  | 2 | // This file is part of Moodle - http://moodle.org/
 | 
        
           |  |  | 3 | //
 | 
        
           |  |  | 4 | // Moodle is free software: you can redistribute it and/or modify
 | 
        
           |  |  | 5 | // it under the terms of the GNU General Public License as published by
 | 
        
           |  |  | 6 | // the Free Software Foundation, either version 3 of the License, or
 | 
        
           |  |  | 7 | // (at your option) any later version.
 | 
        
           |  |  | 8 | //
 | 
        
           |  |  | 9 | // Moodle is distributed in the hope that it will be useful,
 | 
        
           |  |  | 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
        
           |  |  | 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
        
           |  |  | 12 | // GNU General Public License for more details.
 | 
        
           |  |  | 13 | //
 | 
        
           |  |  | 14 | // You should have received a copy of the GNU General Public License
 | 
        
           |  |  | 15 | // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 | 
        
           |  |  | 16 |   | 
        
           |  |  | 17 | declare(strict_types=1);
 | 
        
           |  |  | 18 |   | 
        
           |  |  | 19 | namespace core_reportbuilder\local\helpers;
 | 
        
           |  |  | 20 |   | 
        
           | 1441 | ariadna | 21 | use core\lang_string;
 | 
        
           | 1 | efrain | 22 | use core_text;
 | 
        
           | 1441 | ariadna | 23 | use core_reportbuilder\local\filters\{boolean_select, date, select, text};
 | 
        
           |  |  | 24 | use core_reportbuilder\local\report\{column, filter};
 | 
        
           | 1 | efrain | 25 | use profile_field_base;
 | 
        
           |  |  | 26 | use stdClass;
 | 
        
           |  |  | 27 |   | 
        
           |  |  | 28 | defined('MOODLE_INTERNAL') || die();
 | 
        
           |  |  | 29 |   | 
        
           |  |  | 30 | global $CFG;
 | 
        
           |  |  | 31 | require_once($CFG->dirroot.'/user/profile/lib.php');
 | 
        
           |  |  | 32 |   | 
        
           |  |  | 33 | /**
 | 
        
           |  |  | 34 |  * Helper class for user profile fields.
 | 
        
           |  |  | 35 |  *
 | 
        
           |  |  | 36 |  * @package   core_reportbuilder
 | 
        
           |  |  | 37 |  * @copyright 2021 David Matamoros <davidmc@moodle.com>
 | 
        
           |  |  | 38 |  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 39 |  */
 | 
        
           |  |  | 40 | class user_profile_fields {
 | 
        
           |  |  | 41 |   | 
        
           | 1441 | ariadna | 42 |     use join_trait;
 | 
        
           | 1 | efrain | 43 |   | 
        
           | 1441 | ariadna | 44 |     /** @var profile_field_base[] User profile fields */
 | 
        
           |  |  | 45 |     private array $userprofilefields;
 | 
        
           | 1 | efrain | 46 |   | 
        
           |  |  | 47 |     /**
 | 
        
           | 1441 | ariadna | 48 |      * Constructor
 | 
        
           | 1 | efrain | 49 |      *
 | 
        
           | 1441 | ariadna | 50 |      * @param string $usertablefieldalias The table/field alias to match the user ID when adding columns and filters.
 | 
        
           | 1 | efrain | 51 |      * @param string $entityname The entity name used when adding columns and filters.
 | 
        
           |  |  | 52 |      */
 | 
        
           | 1441 | ariadna | 53 |     public function __construct(
 | 
        
           |  |  | 54 |         /** @var string The table/field alias to match the user ID when adding columns and filters */
 | 
        
           |  |  | 55 |         private readonly string $usertablefieldalias,
 | 
        
           |  |  | 56 |         /** @var string The entity name used when adding columns and filters */
 | 
        
           |  |  | 57 |         private readonly string $entityname,
 | 
        
           |  |  | 58 |     ) {
 | 
        
           |  |  | 59 |         $this->userprofilefields = profile_get_user_fields_with_data(0);
 | 
        
           | 1 | efrain | 60 |     }
 | 
        
           |  |  | 61 |   | 
        
           |  |  | 62 |     /**
 | 
        
           |  |  | 63 |      * Get table alias for given profile field
 | 
        
           |  |  | 64 |      *
 | 
        
           |  |  | 65 |      * The entity name is used to ensure the alias differs when the entity is used multiple times within the same report, each
 | 
        
           |  |  | 66 |      * having their own table alias/join
 | 
        
           |  |  | 67 |      *
 | 
        
           |  |  | 68 |      * @param profile_field_base $profilefield
 | 
        
           |  |  | 69 |      * @return string
 | 
        
           |  |  | 70 |      */
 | 
        
           |  |  | 71 |     private function get_table_alias(profile_field_base $profilefield): string {
 | 
        
           |  |  | 72 |         static $aliases = [];
 | 
        
           |  |  | 73 |   | 
        
           |  |  | 74 |         $aliaskey = "{$this->entityname}_{$profilefield->fieldid}";
 | 
        
           |  |  | 75 |         if (!array_key_exists($aliaskey, $aliases)) {
 | 
        
           |  |  | 76 |             $aliases[$aliaskey] = database::generate_alias();
 | 
        
           |  |  | 77 |         }
 | 
        
           |  |  | 78 |   | 
        
           |  |  | 79 |         return $aliases[$aliaskey];
 | 
        
           |  |  | 80 |     }
 | 
        
           |  |  | 81 |   | 
        
           |  |  | 82 |     /**
 | 
        
           |  |  | 83 |      * Get table join for given profile field
 | 
        
           |  |  | 84 |      *
 | 
        
           |  |  | 85 |      * @param profile_field_base $profilefield
 | 
        
           |  |  | 86 |      * @return string
 | 
        
           |  |  | 87 |      */
 | 
        
           |  |  | 88 |     private function get_table_join(profile_field_base $profilefield): string {
 | 
        
           |  |  | 89 |         $userinfotablealias = $this->get_table_alias($profilefield);
 | 
        
           |  |  | 90 |   | 
        
           |  |  | 91 |         return "LEFT JOIN {user_info_data} {$userinfotablealias}
 | 
        
           |  |  | 92 |                        ON {$userinfotablealias}.userid = {$this->usertablefieldalias}
 | 
        
           |  |  | 93 |                       AND {$userinfotablealias}.fieldid = {$profilefield->fieldid}";
 | 
        
           |  |  | 94 |     }
 | 
        
           |  |  | 95 |   | 
        
           |  |  | 96 |     /**
 | 
        
           |  |  | 97 |      * Return the user profile fields visible columns.
 | 
        
           |  |  | 98 |      *
 | 
        
           |  |  | 99 |      * @return column[]
 | 
        
           |  |  | 100 |      */
 | 
        
           |  |  | 101 |     public function get_columns(): array {
 | 
        
           |  |  | 102 |         global $DB;
 | 
        
           |  |  | 103 |   | 
        
           |  |  | 104 |         $columns = [];
 | 
        
           | 1441 | ariadna | 105 |   | 
        
           | 1 | efrain | 106 |         foreach ($this->userprofilefields as $profilefield) {
 | 
        
           | 1441 | ariadna | 107 |             $userinfotablealias = $this->get_table_alias($profilefield);
 | 
        
           |  |  | 108 |             $userinfosql = "{$userinfotablealias}.data";
 | 
        
           |  |  | 109 |   | 
        
           |  |  | 110 |             // Numeric column (non-text) should coalesce with default, for aggregation.
 | 
        
           | 1 | efrain | 111 |             $columntype = $this->get_user_field_type($profilefield->field->datatype);
 | 
        
           | 1441 | ariadna | 112 |             if (!in_array($columntype, [column::TYPE_TEXT, column::TYPE_LONGTEXT])) {
 | 
        
           | 1 | efrain | 113 |   | 
        
           | 1441 | ariadna | 114 |                 // See MDL-78783 regarding no bound parameters, and SQL Server limitations of GROUP BY.
 | 
        
           |  |  | 115 |                 $userinfosql = "
 | 
        
           |  |  | 116 |                     CASE WHEN {$this->usertablefieldalias} IS NOT NULL
 | 
        
           |  |  | 117 |                          THEN " .
 | 
        
           |  |  | 118 |                             $DB->sql_cast_char2int("COALESCE({$userinfosql}, '" . (float) $profilefield->field->defaultdata . "')")
 | 
        
           |  |  | 119 |                             . "
 | 
        
           |  |  | 120 |                          ELSE NULL
 | 
        
           |  |  | 121 |                     END";
 | 
        
           | 1 | efrain | 122 |             }
 | 
        
           |  |  | 123 |   | 
        
           | 1441 | ariadna | 124 |             $columnname = 'profilefield_' . core_text::strtolower($profilefield->field->shortname);
 | 
        
           |  |  | 125 |             $columns[$columnname] = (new column(
 | 
        
           |  |  | 126 |                 $columnname,
 | 
        
           |  |  | 127 |                 new lang_string('customfieldcolumn', 'core_reportbuilder', $profilefield->display_name(false)),
 | 
        
           | 1 | efrain | 128 |                 $this->entityname
 | 
        
           |  |  | 129 |             ))
 | 
        
           |  |  | 130 |                 ->add_joins($this->get_joins())
 | 
        
           |  |  | 131 |                 ->add_join($this->get_table_join($profilefield))
 | 
        
           |  |  | 132 |                 ->set_type($columntype)
 | 
        
           | 1441 | ariadna | 133 |                 ->add_field($userinfosql, 'data')
 | 
        
           |  |  | 134 |                 ->add_field("{$userinfotablealias}.dataformat")
 | 
        
           |  |  | 135 |                 ->add_field($this->usertablefieldalias, 'userid')
 | 
        
           |  |  | 136 |                 ->set_is_sortable(true)
 | 
        
           | 1 | efrain | 137 |                 ->add_callback(static function($value, stdClass $row, profile_field_base $field): string {
 | 
        
           | 1441 | ariadna | 138 |                     if ($row->userid === null && $value === null) {
 | 
        
           | 1 | efrain | 139 |                         return '';
 | 
        
           |  |  | 140 |                     }
 | 
        
           |  |  | 141 |   | 
        
           | 1441 | ariadna | 142 |                     $field->set_user_data(
 | 
        
           |  |  | 143 |                         $row->data ?? $field->field->defaultdata,
 | 
        
           |  |  | 144 |                         $row->dataformat ?? $field->field->defaultdataformat,
 | 
        
           |  |  | 145 |                     );
 | 
        
           |  |  | 146 |   | 
        
           |  |  | 147 |                     return $field->display_data();
 | 
        
           |  |  | 148 |                 }, $profilefield)
 | 
        
           |  |  | 149 |                 ->set_is_available($profilefield->is_visible());
 | 
        
           | 1 | efrain | 150 |         }
 | 
        
           |  |  | 151 |   | 
        
           | 1441 | ariadna | 152 |         return array_values($columns);
 | 
        
           | 1 | efrain | 153 |     }
 | 
        
           |  |  | 154 |   | 
        
           |  |  | 155 |     /**
 | 
        
           |  |  | 156 |      * Get custom user profile fields filters.
 | 
        
           |  |  | 157 |      *
 | 
        
           |  |  | 158 |      * @return filter[]
 | 
        
           |  |  | 159 |      */
 | 
        
           |  |  | 160 |     public function get_filters(): array {
 | 
        
           |  |  | 161 |         global $DB;
 | 
        
           |  |  | 162 |   | 
        
           |  |  | 163 |         $filters = [];
 | 
        
           | 1441 | ariadna | 164 |   | 
        
           | 1 | efrain | 165 |         foreach ($this->userprofilefields as $profilefield) {
 | 
        
           | 1441 | ariadna | 166 |             $userinfotablealias = $this->get_table_alias($profilefield);
 | 
        
           |  |  | 167 |             $userinfosql = "{$userinfotablealias}.data";
 | 
        
           |  |  | 168 |             $userinfoparams = [];
 | 
        
           | 1 | efrain | 169 |   | 
        
           | 1441 | ariadna | 170 |             // Perform casts where necessary, as this is a text DB field.
 | 
        
           | 1 | efrain | 171 |             switch ($profilefield->field->datatype) {
 | 
        
           |  |  | 172 |                 case 'checkbox':
 | 
        
           |  |  | 173 |                     $classname = boolean_select::class;
 | 
        
           | 1441 | ariadna | 174 |                     $userinfosql = $DB->sql_cast_char2int($userinfosql, true);
 | 
        
           | 1 | efrain | 175 |                     break;
 | 
        
           |  |  | 176 |                 case 'datetime':
 | 
        
           |  |  | 177 |                     $classname = date::class;
 | 
        
           | 1441 | ariadna | 178 |                     $userinfosql = $DB->sql_cast_char2int($userinfosql, true);
 | 
        
           | 1 | efrain | 179 |                     break;
 | 
        
           |  |  | 180 |                 case 'menu':
 | 
        
           |  |  | 181 |                     $classname = select::class;
 | 
        
           |  |  | 182 |                     break;
 | 
        
           |  |  | 183 |                 case 'text':
 | 
        
           |  |  | 184 |                 case 'textarea':
 | 
        
           |  |  | 185 |                 default:
 | 
        
           |  |  | 186 |                     $classname = text::class;
 | 
        
           |  |  | 187 |                     break;
 | 
        
           |  |  | 188 |             }
 | 
        
           |  |  | 189 |   | 
        
           | 1441 | ariadna | 190 |             // Account for field default value, when joined to the user table.
 | 
        
           |  |  | 191 |             if (($fielddefault = $profilefield->field->defaultdata) !== null) {
 | 
        
           |  |  | 192 |                 $paramdefault = database::generate_param_name();
 | 
        
           |  |  | 193 |                 $userinfosql = "
 | 
        
           |  |  | 194 |                         CASE WHEN {$this->usertablefieldalias} IS NOT NULL
 | 
        
           |  |  | 195 |                              THEN COALESCE({$userinfosql}, :{$paramdefault})
 | 
        
           |  |  | 196 |                              ELSE NULL
 | 
        
           |  |  | 197 |                         END";
 | 
        
           |  |  | 198 |                 $userinfoparams[$paramdefault] = $fielddefault;
 | 
        
           |  |  | 199 |             }
 | 
        
           |  |  | 200 |   | 
        
           |  |  | 201 |             $filtername = 'profilefield_' . core_text::strtolower($profilefield->field->shortname);
 | 
        
           | 1 | efrain | 202 |             $filter = (new filter(
 | 
        
           |  |  | 203 |                 $classname,
 | 
        
           | 1441 | ariadna | 204 |                 $filtername,
 | 
        
           |  |  | 205 |                 new lang_string('customfieldcolumn', 'core_reportbuilder', $profilefield->display_name(false)),
 | 
        
           | 1 | efrain | 206 |                 $this->entityname,
 | 
        
           | 1441 | ariadna | 207 |                 $userinfosql,
 | 
        
           |  |  | 208 |                 $userinfoparams,
 | 
        
           | 1 | efrain | 209 |             ))
 | 
        
           |  |  | 210 |                 ->add_joins($this->get_joins())
 | 
        
           | 1441 | ariadna | 211 |                 ->add_join($this->get_table_join($profilefield))
 | 
        
           |  |  | 212 |                 ->set_is_available($profilefield->is_visible());
 | 
        
           | 1 | efrain | 213 |   | 
        
           | 1441 | ariadna | 214 |             // If using a select filter, then populate the options.
 | 
        
           |  |  | 215 |             if ($filter->get_filter_class() === select::class) {
 | 
        
           |  |  | 216 |                 $filter->set_options_callback(fn(): array => $profilefield->options);
 | 
        
           | 1 | efrain | 217 |             }
 | 
        
           |  |  | 218 |   | 
        
           | 1441 | ariadna | 219 |             $filters[$filtername] = $filter;
 | 
        
           | 1 | efrain | 220 |         }
 | 
        
           |  |  | 221 |   | 
        
           | 1441 | ariadna | 222 |         return array_values($filters);
 | 
        
           | 1 | efrain | 223 |     }
 | 
        
           |  |  | 224 |   | 
        
           |  |  | 225 |     /**
 | 
        
           |  |  | 226 |      * Get user profile field type for report.
 | 
        
           |  |  | 227 |      *
 | 
        
           |  |  | 228 |      * @param string $userfield user field.
 | 
        
           |  |  | 229 |      * @return int the constant equivalent to this custom field type.
 | 
        
           |  |  | 230 |      */
 | 
        
           |  |  | 231 |     protected function get_user_field_type(string $userfield): int {
 | 
        
           |  |  | 232 |         switch ($userfield) {
 | 
        
           |  |  | 233 |             case 'checkbox':
 | 
        
           |  |  | 234 |                 $customfieldtype = column::TYPE_BOOLEAN;
 | 
        
           |  |  | 235 |                 break;
 | 
        
           |  |  | 236 |             case 'datetime':
 | 
        
           |  |  | 237 |                 $customfieldtype = column::TYPE_TIMESTAMP;
 | 
        
           |  |  | 238 |                 break;
 | 
        
           |  |  | 239 |             case 'textarea':
 | 
        
           |  |  | 240 |                 $customfieldtype = column::TYPE_LONGTEXT;
 | 
        
           |  |  | 241 |                 break;
 | 
        
           |  |  | 242 |             case 'menu':
 | 
        
           |  |  | 243 |             case 'text':
 | 
        
           |  |  | 244 |             default:
 | 
        
           |  |  | 245 |                 $customfieldtype = column::TYPE_TEXT;
 | 
        
           |  |  | 246 |                 break;
 | 
        
           |  |  | 247 |         }
 | 
        
           |  |  | 248 |         return $customfieldtype;
 | 
        
           |  |  | 249 |     }
 | 
        
           |  |  | 250 | }
 |