Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

Differences Between: [Versions 400 and 402] [Versions 400 and 403]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  namespace qbank_customfields\customfield;
  18  
  19  use core_customfield\api;
  20  use core_customfield\field_controller;
  21  use core_customfield\output\field_data;
  22  
  23  /**
  24   * Question handler for custom fields.
  25   *
  26   * @package     qbank_customfields
  27   * @copyright   2021 Catalyst IT Australia Pty Ltd
  28   * @author      Matt Porritt <mattp@catalyst-au.net>
  29   * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  30   */
  31  class question_handler extends \core_customfield\handler {
  32  
  33      /**
  34       * @var question_handler
  35       */
  36      static protected $singleton;
  37  
  38      /**
  39       * @var \context
  40       */
  41      protected $parentcontext;
  42  
  43      /** @var int Field is displayed in the question display and question preview, visible to everybody */
  44      const VISIBLETOALL = 2;
  45      /** @var int Field is displayed in the question display and question preview but only for "teachers" */
  46      const VISIBLETOTEACHERS = 1;
  47      /** @var int Field is not displayed in the question display and question preview */
  48      const NOTVISIBLE = 0;
  49  
  50      /**
  51       * Creates the custom field handler and returns a singleton.
  52       * Itemid is always zero as the custom fields are the same
  53       * for every question across the system.
  54       *
  55       * @param int $itemid Always zero.
  56       * @return \qbank_customfields\customfield\question_handler
  57       */
  58      public static function create(int $itemid = 0) : \core_customfield\handler {
  59          if (static::$singleton === null) {
  60              self::$singleton = new static(0);
  61          }
  62          return self::$singleton;
  63      }
  64  
  65      /**
  66       * Run reset code after unit tests to reset the singleton usage.
  67       */
  68      public static function reset_caches(): void {
  69          if (!PHPUNIT_TEST) {
  70              throw new \coding_exception('This feature is only intended for use in unit tests');
  71          }
  72  
  73          static::$singleton = null;
  74      }
  75  
  76      /**
  77       * The current user can configure custom fields on this component.
  78       *
  79       * @return bool true if the current can configure custom fields, false otherwise
  80       */
  81      public function can_configure() : bool {
  82          return has_capability('qbank/customfields:configurecustomfields', $this->get_configuration_context());
  83      }
  84  
  85      /**
  86       * The current user can edit custom fields for the given question.
  87       *
  88       * @param field_controller $field
  89       * @param int $instanceid id of the question to test edit permission
  90       * @return bool true if the current can edit custom fields, false otherwise
  91       */
  92      public function can_edit(field_controller $field, int $instanceid = 0) : bool {
  93          if ($instanceid) {
  94              $context = $this->get_instance_context($instanceid);
  95          } else {
  96              $context = $this->get_parent_context();
  97          }
  98  
  99          return (!$field->get_configdata_property('locked') ||
 100                  has_capability('qbank/customfields:changelockedcustomfields', $context));
 101      }
 102  
 103      /**
 104       * The current user can view custom fields for the given question.
 105       *
 106       * @param field_controller $field
 107       * @param int $instanceid id of the question to test edit permission
 108       * @return bool true if the current can edit custom fields, false otherwise
 109       */
 110      public function can_view(field_controller $field, int $instanceid) : bool {
 111          $visibility = $field->get_configdata_property('visibility');
 112          if ($visibility == self::NOTVISIBLE) {
 113              return false;
 114          } else if ($visibility == self::VISIBLETOTEACHERS) {
 115              return has_capability('qbank/customfields:viewhiddencustomfields', $this->get_instance_context($instanceid));
 116          } else {
 117              return true;
 118          }
 119      }
 120  
 121      /**
 122       * Determine if the current user can view custom field in their given context.
 123       * This determines if the user can see the field at all not just the field for
 124       * a particular instance.
 125       * Used primarily in showing or not the field in the question bank table.
 126       *
 127       * @param field_controller $field The field trying to be viewed.
 128       * @param context $context The context the field is being displayed in.
 129       * @return bool true if the current can edit custom fields, false otherwise.
 130       */
 131      public function can_view_type(field_controller $field, \context $context) : bool {
 132          $visibility = $field->get_configdata_property('visibility');
 133          if ($visibility == self::NOTVISIBLE) {
 134              return false;
 135          } else if ($visibility == self::VISIBLETOTEACHERS) {
 136              return has_capability('qbank/customfields:viewhiddencustomfields', $context);
 137          } else {
 138              return true;
 139          }
 140      }
 141  
 142      /**
 143       * Sets parent context for the question.
 144       *
 145       * This may be needed when question is being created, there is no question context but we need to check capabilities
 146       *
 147       * @param \context $context
 148       */
 149      public function set_parent_context(\context $context): void {
 150          $this->parentcontext = $context;
 151      }
 152  
 153      /**
 154       * Returns the parent context for the question.
 155       *
 156       * @return \context
 157       */
 158      protected function get_parent_context() : \context {
 159          if ($this->parentcontext) {
 160              return $this->parentcontext;
 161          } else {
 162              return \context_system::instance();
 163          }
 164      }
 165  
 166      /**
 167       * Context that should be used for new categories created by this handler.
 168       *
 169       * @return \context the context for configuration
 170       */
 171      public function get_configuration_context() : \context {
 172          return \context_system::instance();
 173      }
 174  
 175      /**
 176       * URL for configuration page for the fields for the question custom fields.
 177       *
 178       * @return \moodle_url The URL to configure custom fields for this component
 179       */
 180      public function get_configuration_url() : \moodle_url {
 181          return new \moodle_url('/question/customfield.php');
 182      }
 183  
 184      /**
 185       * Returns the context for the data associated with the given instanceid.
 186       *
 187       * @param int $instanceid id of the record to get the context for
 188       * @return \context the context for the given record
 189       * @throws \coding_exception
 190       */
 191      public function get_instance_context(int $instanceid = 0) : \context {
 192          if ($instanceid > 0) {
 193              $questiondata = \question_bank::load_question_data($instanceid);
 194              $contextid = $questiondata->contextid;
 195              $context = \context::instance_by_id($contextid);
 196              return $context;
 197          } else {
 198              throw new \coding_exception('Instance id must be provided.');
 199          }
 200      }
 201  
 202      /**
 203       * Given a field and instance id get all the filed data.
 204       *
 205       * @param field_controller $field The field to get the data for.
 206       * @param int $instanceid The instance id to get the data for.
 207       * @return \core_customfield\data_controller The fetched data.
 208       */
 209      public function get_field_data(\core_customfield\field_controller $field, int $instanceid): \core_customfield\data_controller {
 210          $fields = [$field->get('id') => $field];
 211          $fieldsdata = api::get_instance_fields_data($fields, $instanceid);
 212          return $fieldsdata[$field->get('id')];
 213      }
 214  
 215      /**
 216       * For a given instance id (question id) get the categories and the
 217       * fields with any data. Return an array of categories containing an
 218       * array of field names and values that is ready to be passed to a renderer.
 219       *
 220       * @param int $instanceid The instance id to get the data for.
 221       * @return array $cfdata The fetched data
 222       */
 223      public function get_categories_fields_data(int $instanceid): array {
 224          // Prepare custom fields data.
 225          $instancedata = $this->get_instance_data($instanceid);
 226  
 227          $cfdata = [];
 228  
 229          foreach ($instancedata as $instance) {
 230              $field = $instance->get_field();
 231  
 232              if ($this->can_view($field, $instanceid)) {
 233                  $category = $instance->get_field()->get_category()->get('name');
 234                  $fieldname = $field->get_formatted_name();
 235                  $fieldvalue = $this->get_field_data($field, $instanceid)->export_value();
 236                  $cfdata[$category][] = ['name' => $fieldname,  'value' => $fieldvalue];
 237              }
 238  
 239          }
 240  
 241          return $cfdata;
 242      }
 243  
 244      /**
 245       * Get the custom data for the given field
 246       * and render HTML ready for display in question table.
 247       *
 248       * @param object $fielddata The field data used for display.
 249       * @return string The HTML to display in the table column.
 250       */
 251      public function display_custom_field_table(object $fielddata) : string {
 252          global $PAGE;
 253  
 254          $output = $PAGE->get_renderer('qbank_customfields');
 255          $outputdata = new field_data($fielddata);
 256  
 257          return $output->render_for_table($outputdata);
 258      }
 259  
 260      /**
 261       * Render the custom field category and filed data as HTML ready for display.
 262       *
 263       * @param array $catfielddata Array of categories and field names and values.
 264       * @return string The HTML to display.
 265       */
 266      public function display_custom_categories_fields(array $catfielddata) : string {
 267          global $PAGE;
 268          $output = $PAGE->get_renderer('qbank_customfields');
 269  
 270          return $output->render_for_preview($catfielddata);
 271      }
 272  
 273      /**
 274       * Add custom controls to the field configuration form that will be saved.
 275       *
 276       * @param \MoodleQuickForm $mform The form to add the custom fields to.
 277       */
 278      public function config_form_definition(\MoodleQuickForm $mform): void {
 279          $mform->addElement('header', 'question_handler_header',
 280                  get_string('customfieldsettings', 'qbank_customfields'));
 281          $mform->setExpanded('question_handler_header', true);
 282  
 283          // If field is locked.
 284          $mform->addElement('selectyesno', 'configdata[locked]',
 285                  get_string('customfield_islocked', 'qbank_customfields'));
 286          $mform->addHelpButton('configdata[locked]', 'customfield_islocked', 'qbank_customfields');
 287  
 288          // Field data visibility.
 289          $visibilityoptions = [
 290                  self::VISIBLETOALL => get_string('customfield_visibletoall', 'qbank_customfields'),
 291                  self::VISIBLETOTEACHERS => get_string('customfield_visibletoteachers', 'qbank_customfields'),
 292                  self::NOTVISIBLE => get_string('customfield_notvisible', 'qbank_customfields')
 293          ];
 294          $mform->addElement('select', 'configdata[visibility]',
 295                  get_string('customfield_visibility', 'qbank_customfields'),
 296                  $visibilityoptions);
 297          $mform->addHelpButton(
 298                  'configdata[visibility]', 'customfield_visibility', 'qbank_customfields');
 299      }
 300  
 301      /**
 302       * Creates or updates the question custom field data when restoring from a backup.
 303       *
 304       * @param \restore_task $task
 305       * @param array $data
 306       */
 307      public function restore_instance_data_from_backup(\restore_task $task, array $data): void {
 308  
 309          $editablefields = $this->get_editable_fields($data['newquestion']);
 310          $records = api::get_instance_fields_data($editablefields, $data['newquestion']);
 311          $target = $task->get_target();
 312          $override = ($target != \backup::TARGET_CURRENT_ADDING && $target != \backup::TARGET_EXISTING_ADDING);
 313  
 314          foreach ($records as $d) {
 315              $field = $d->get_field();
 316              if ($field->get('shortname') === $data['shortname'] && $field->get('type') === $data['type']) {
 317                  if (!$d->get('id') || $override) {
 318                      $d->set($d->datafield(), $data['value']);
 319                      $d->set('value', $data['value']);
 320                      $d->set('valueformat', $data['valueformat']);
 321                      $d->set('contextid', $data['fieldcontextid']);
 322                      $d->save();
 323                  }
 324                  return;
 325              }
 326          }
 327      }
 328  }