Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 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  /**
  18   * Contains class mod_feedback_complete_form
  19   *
  20   * @package   mod_feedback
  21   * @copyright 2016 Marina Glancy
  22   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  defined('MOODLE_INTERNAL') || die();
  26  
  27  /**
  28   * Class mod_feedback_complete_form
  29   *
  30   * @package   mod_feedback
  31   * @copyright 2016 Marina Glancy
  32   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  33   */
  34  class mod_feedback_complete_form extends moodleform {
  35  
  36      /** @var int */
  37      const MODE_COMPLETE = 1;
  38      /** @var int */
  39      const MODE_PRINT = 2;
  40      /** @var int */
  41      const MODE_EDIT = 3;
  42      /** @var int */
  43      const MODE_VIEW_RESPONSE = 4;
  44      /** @var int */
  45      const MODE_VIEW_TEMPLATE = 5;
  46  
  47      /** @var int */
  48      protected $mode;
  49      /** @var mod_feedback_structure|mod_feedback_completion */
  50      protected $structure;
  51      /** @var mod_feedback_completion */
  52      protected $completion;
  53      /** @var int */
  54      protected $gopage;
  55      /** @var bool */
  56      protected $hasrequired = false;
  57  
  58      /**
  59       * Constructor
  60       *
  61       * @param int $mode
  62       * @param mod_feedback_structure $structure
  63       * @param string $formid CSS id attribute of the form
  64       * @param array $customdata
  65       */
  66      public function __construct($mode, mod_feedback_structure $structure, $formid, $customdata = null) {
  67          $this->mode = $mode;
  68          $this->structure = $structure;
  69          $this->gopage = isset($customdata['gopage']) ? $customdata['gopage'] : 0;
  70          $isanonymous = $this->structure->is_anonymous() ? ' ianonymous' : '';
  71          parent::__construct(null, $customdata, 'POST', '',
  72                  array('id' => $formid, 'class' => 'feedback_form' . $isanonymous), true);
  73          $this->set_display_vertical();
  74      }
  75  
  76      /**
  77       * Form definition
  78       */
  79      public function definition() {
  80          $mform = $this->_form;
  81          $mform->addElement('hidden', 'id', $this->get_cm()->id);
  82          $mform->setType('id', PARAM_INT);
  83          $mform->addElement('hidden', 'courseid', $this->get_current_course_id());
  84          $mform->setType('courseid', PARAM_INT);
  85          $mform->addElement('hidden', 'gopage');
  86          $mform->setType('gopage', PARAM_INT);
  87          $mform->addElement('hidden', 'lastpage');
  88          $mform->setType('lastpage', PARAM_INT);
  89          $mform->addElement('hidden', 'startitempos');
  90          $mform->setType('startitempos', PARAM_INT);
  91          $mform->addElement('hidden', 'lastitempos');
  92          $mform->setType('lastitempos', PARAM_INT);
  93  
  94          if (isloggedin() && !isguestuser() && $this->mode != self::MODE_EDIT && $this->mode != self::MODE_VIEW_TEMPLATE &&
  95                      $this->mode != self::MODE_VIEW_RESPONSE) {
  96              // Output information about the current mode (anonymous or not) in some modes.
  97              if ($this->structure->is_anonymous()) {
  98                  $anonymousmodeinfo = get_string('anonymous', 'feedback');
  99              } else {
 100                  $anonymousmodeinfo = get_string('non_anonymous', 'feedback');
 101              }
 102              $element = $mform->addElement('static', 'anonymousmode', '',
 103                      get_string('mode', 'feedback') . ': ' . $anonymousmodeinfo);
 104              $element->setAttributes($element->getAttributes() + ['class' => 'feedback_mode']);
 105          }
 106  
 107          // Add buttons to go to previous/next pages and submit the feedback.
 108          if ($this->mode == self::MODE_COMPLETE) {
 109              $buttonarray = array();
 110              $buttonarray[] = &$mform->createElement('submit', 'gopreviouspage', get_string('previous_page', 'feedback'));
 111              $buttonarray[] = &$mform->createElement('submit', 'gonextpage', get_string('next_page', 'feedback'),
 112                      array('class' => 'form-submit'));
 113              $buttonarray[] = &$mform->createElement('submit', 'savevalues', get_string('save_entries', 'feedback'),
 114                      array('class' => 'form-submit'));
 115              $buttonarray[] = &$mform->createElement('cancel');
 116              $mform->addGroup($buttonarray, 'buttonar', '', array(' '), false);
 117              $mform->closeHeaderBefore('buttonar');
 118          }
 119  
 120          if ($this->mode == self::MODE_COMPLETE) {
 121              $this->definition_complete();
 122          } else {
 123              $this->definition_preview();
 124          }
 125  
 126          // Set data.
 127          $this->set_data(array('gopage' => $this->gopage));
 128      }
 129  
 130      /**
 131       * Called from definition_after_data() in the completion mode
 132       *
 133       * This will add only items from a current page to the feedback and adjust the buttons
 134       */
 135      protected function definition_complete() {
 136          if (!$this->structure instanceof mod_feedback_completion) {
 137              // We should not really be here but just in case.
 138              return;
 139          }
 140          $pages = $this->structure->get_pages();
 141          $gopage = $this->gopage;
 142          $pageitems = $pages[$gopage];
 143          $hasnextpage = $gopage < count($pages) - 1; // Until we complete this page we can not trust get_next_page().
 144          $hasprevpage = $gopage && ($this->structure->get_previous_page($gopage, false) !== null);
 145  
 146          // Add elements.
 147          foreach ($pageitems as $item) {
 148              $itemobj = feedback_get_item_class($item->typ);
 149              $itemobj->complete_form_element($item, $this);
 150          }
 151  
 152          // Remove invalid buttons (for example, no "previous page" if we are on the first page).
 153          if (!$hasprevpage) {
 154              $this->remove_button('gopreviouspage');
 155          }
 156          if (!$hasnextpage) {
 157              $this->remove_button('gonextpage');
 158          }
 159          if ($hasnextpage) {
 160              $this->remove_button('savevalues');
 161          }
 162      }
 163  
 164      /**
 165       * Called from definition_after_data() in all modes except for completion
 166       *
 167       * This will add all items to the form, including pagebreaks as horizontal rules.
 168       */
 169      protected function definition_preview() {
 170          foreach ($this->structure->get_items() as $feedbackitem) {
 171              $itemobj = feedback_get_item_class($feedbackitem->typ);
 172              $itemobj->complete_form_element($feedbackitem, $this);
 173          }
 174      }
 175  
 176      /**
 177       * Removes the button that is not applicable for the current page
 178       *
 179       * @param string $buttonname
 180       */
 181      private function remove_button($buttonname) {
 182          $el = $this->_form->getElement('buttonar');
 183          foreach ($el->_elements as $idx => $button) {
 184              if ($button instanceof MoodleQuickForm_submit && $button->getName() === $buttonname) {
 185                  unset($el->_elements[$idx]);
 186                  return;
 187              }
 188          }
 189      }
 190  
 191      /**
 192       * Returns value for this element that is already stored in temporary or permanent table,
 193       * usually only available when user clicked "Previous page". Null means no value is stored.
 194       *
 195       * @param stdClass $item
 196       * @return string
 197       */
 198      public function get_item_value($item) {
 199          if ($this->structure instanceof mod_feedback_completion) {
 200              return $this->structure->get_item_value($item);
 201          }
 202          return null;
 203      }
 204  
 205      /**
 206       * Can be used by the items to get the course id for which feedback is taken
 207       *
 208       * This function returns 0 for feedbacks that are located inside the courses.
 209       * $this->get_feedback()->course will return the course where feedback is located.
 210       * $this->get_current_course_id() will return the course where user was before taking the feedback
 211       *
 212       * @return int
 213       */
 214      public function get_course_id() {
 215          return $this->structure->get_courseid();
 216      }
 217  
 218      /**
 219       * Record from 'feedback' table corresponding to the current feedback
 220       * @return stdClass
 221       */
 222      public function get_feedback() {
 223          return $this->structure->get_feedback();
 224      }
 225  
 226      /**
 227       * Current feedback mode, see constants on the top of this class
 228       * @return int
 229       */
 230      public function get_mode() {
 231          return $this->mode;
 232      }
 233  
 234      /**
 235       * Returns whether the form is frozen, some items may prefer to change the element
 236       * type in case of frozen form. For example, text or textarea element does not look
 237       * nice when frozen
 238       *
 239       * @return bool
 240       */
 241      public function is_frozen() {
 242          return $this->mode == self::MODE_VIEW_RESPONSE;
 243      }
 244  
 245      /**
 246       * Returns the current course module
 247       * @return cm_info
 248       */
 249      public function get_cm() {
 250          return $this->structure->get_cm();
 251      }
 252  
 253      /**
 254       * Returns the course where user was before taking the feedback.
 255       *
 256       * For feedbacks inside the course it will be the same as $this->get_feedback()->course.
 257       * For feedbacks on the frontpage it will be the same as $this->get_course_id()
 258       *
 259       * @return int
 260       */
 261      public function get_current_course_id() {
 262          return $this->structure->get_courseid() ?: $this->get_feedback()->course;
 263      }
 264  
 265      /**
 266       * CSS class for the item
 267       * @param stdClass $item
 268       * @return string
 269       */
 270      protected function get_suggested_class($item) {
 271          $class = "feedback_itemlist feedback-item-{$item->typ}";
 272          if ($item->dependitem) {
 273              $class .= " feedback_is_dependent";
 274          }
 275          if ($item->typ !== 'pagebreak') {
 276              $itemobj = feedback_get_item_class($item->typ);
 277              if ($itemobj->get_hasvalue()) {
 278                  $class .= " feedback_hasvalue";
 279              }
 280          }
 281          return $class;
 282      }
 283  
 284      /**
 285       * Adds an element to this form - to be used by items in their complete_form_element() method
 286       *
 287       * @param stdClass $item
 288       * @param HTML_QuickForm_element|array $element either completed form element or an array that
 289       *      can be passed as arguments to $this->_form->createElement() function
 290       * @param bool $addrequiredrule automatically add 'required' rule
 291       * @param bool $setdefaultvalue automatically set default value for element
 292       * @return HTML_QuickForm_element
 293       */
 294      public function add_form_element($item, $element, $addrequiredrule = true, $setdefaultvalue = true) {
 295          global $OUTPUT;
 296  
 297          if (is_array($element) && $element[0] == 'group') {
 298              // For groups, use the mforms addGroup API.
 299              // $element looks like: ['group', $groupinputname, $name, $objects, $separator, $appendname],
 300              $element = $this->_form->addGroup($element[3], $element[1], $element[2], $element[4], $element[5]);
 301          } else {
 302              // Add non-group element to the form.
 303              if (is_array($element)) {
 304                  if ($this->is_frozen() && $element[0] === 'text') {
 305                      // Convert 'text' element to 'static' when freezing for better display.
 306                      $element = ['static', $element[1], $element[2]];
 307                  }
 308                  $element = call_user_func_array(array($this->_form, 'createElement'), $element);
 309              }
 310              $element = $this->_form->addElement($element);
 311          }
 312  
 313          // Prepend standard CSS classes to the element classes.
 314          $attributes = $element->getAttributes();
 315          $class = !empty($attributes['class']) ? ' ' . $attributes['class'] : '';
 316          $attributes['class'] = $this->get_suggested_class($item) . $class;
 317          $element->setAttributes($attributes);
 318  
 319          // Add required rule.
 320          if ($item->required && $addrequiredrule) {
 321              $this->_form->addRule($element->getName(), get_string('required'), 'required', null, 'client');
 322          }
 323  
 324          // Set default value.
 325          if ($setdefaultvalue && ($tmpvalue = $this->get_item_value($item))) {
 326              $this->_form->setDefault($element->getName(), s($tmpvalue));
 327          }
 328  
 329          // Freeze if needed.
 330          if ($this->is_frozen()) {
 331              $element->freeze();
 332          }
 333  
 334          // Add red asterisks on required fields.
 335          if ($item->required) {
 336              $required = $OUTPUT->pix_icon('req', get_string('requiredelement', 'form'));
 337              $element->setLabel($element->getLabel() . $required);
 338              $this->hasrequired = true;
 339          }
 340  
 341          // Add different useful stuff to the question name.
 342          $this->add_item_label($item, $element);
 343          $this->add_item_dependencies($item, $element);
 344          $this->add_item_number($item, $element);
 345  
 346          if ($this->mode == self::MODE_EDIT) {
 347              $this->enhance_name_for_edit($item, $element);
 348          }
 349  
 350          return $element;
 351      }
 352  
 353      /**
 354       * Adds a group element to this form - to be used by items in their complete_form_element() method
 355       *
 356       * @param stdClass $item
 357       * @param string $groupinputname name for the form element
 358       * @param string $name question text
 359       * @param array $elements array of arrays that can be passed to $this->_form->createElement()
 360       * @param string $separator separator between group elements
 361       * @param string $class additional CSS classes for the form element
 362       * @return HTML_QuickForm_element
 363       */
 364      public function add_form_group_element($item, $groupinputname, $name, $elements, $separator,
 365              $class = '') {
 366          $objects = array();
 367          foreach ($elements as $element) {
 368              $object = call_user_func_array(array($this->_form, 'createElement'), $element);
 369              $objects[] = $object;
 370          }
 371          $element = $this->add_form_element($item,
 372                  ['group', $groupinputname, $name, $objects, $separator, false],
 373                  false,
 374                  false);
 375          if ($class !== '') {
 376              $attributes = $element->getAttributes();
 377              $attributes['class'] .= ' ' . $class;
 378              $element->setAttributes($attributes);
 379          }
 380          return $element;
 381      }
 382  
 383      /**
 384       * Adds an item number to the question name (if feedback autonumbering is on)
 385       * @param stdClass $item
 386       * @param HTML_QuickForm_element $element
 387       */
 388      protected function add_item_number($item, $element) {
 389          if ($this->get_feedback()->autonumbering && !empty($item->itemnr)) {
 390              $name = $element->getLabel();
 391              $element->setLabel(html_writer::span($item->itemnr. '.', 'itemnr') . ' ' . $name);
 392          }
 393      }
 394  
 395      /**
 396       * Adds an item label to the question name
 397       * @param stdClass $item
 398       * @param HTML_QuickForm_element $element
 399       */
 400      protected function add_item_label($item, $element) {
 401          if (strlen($item->label) && ($this->mode == self::MODE_EDIT || $this->mode == self::MODE_VIEW_TEMPLATE)) {
 402              $name = get_string('nameandlabelformat', 'mod_feedback',
 403                  (object)['label' => format_string($item->label), 'name' => $element->getLabel()]);
 404              $element->setLabel($name);
 405          }
 406      }
 407  
 408      /**
 409       * Adds a dependency description to the question name
 410       * @param stdClass $item
 411       * @param HTML_QuickForm_element $element
 412       */
 413      protected function add_item_dependencies($item, $element) {
 414          $allitems = $this->structure->get_items();
 415          if ($item->dependitem && ($this->mode == self::MODE_EDIT || $this->mode == self::MODE_VIEW_TEMPLATE)) {
 416              if (isset($allitems[$item->dependitem])) {
 417                  $dependitem = $allitems[$item->dependitem];
 418                  $name = $element->getLabel();
 419                  $name .= html_writer::span(' ('.format_string($dependitem->label).'-&gt;'.$item->dependvalue.')',
 420                          'feedback_depend');
 421                  $element->setLabel($name);
 422              }
 423          }
 424      }
 425  
 426      /**
 427       * Returns the CSS id attribute that will be assigned by moodleform later to this element
 428       * @param stdClass $item
 429       * @param HTML_QuickForm_element $element
 430       */
 431      protected function guess_element_id($item, $element) {
 432          if (!$id = $element->getAttribute('id')) {
 433              $attributes = $element->getAttributes();
 434              $id = $attributes['id'] = 'feedback_item_' . $item->id;
 435              $element->setAttributes($attributes);
 436          }
 437          if ($element->getType() === 'group') {
 438              return 'fgroup_' . $id;
 439          }
 440          return 'fitem_' . $id;
 441      }
 442  
 443      /**
 444       * Adds editing actions to the question name in the edit mode
 445       * @param stdClass $item
 446       * @param HTML_QuickForm_element $element
 447       */
 448      protected function enhance_name_for_edit($item, $element) {
 449          global $OUTPUT;
 450          $menu = new action_menu();
 451          $menu->set_owner_selector('#' . $this->guess_element_id($item, $element));
 452          $menu->set_constraint('.feedback_form');
 453          $menu->set_alignment(action_menu::TR, action_menu::BR);
 454          $menu->set_menu_trigger(get_string('edit'));
 455          $menu->prioritise = true;
 456  
 457          $itemobj = feedback_get_item_class($item->typ);
 458          $actions = $itemobj->edit_actions($item, $this->get_feedback(), $this->get_cm());
 459          foreach ($actions as $action) {
 460              $menu->add($action);
 461          }
 462          $editmenu = $OUTPUT->render($menu);
 463  
 464          $name = $element->getLabel();
 465  
 466          $name = html_writer::span('', 'itemdd', array('id' => 'feedback_item_box_' . $item->id)) .
 467                  html_writer::span($name, 'itemname') .
 468                  html_writer::span($editmenu, 'itemactions');
 469          $element->setLabel(html_writer::span($name, 'itemtitle'));
 470      }
 471  
 472      /**
 473       * Sets the default value for form element - alias to $this->_form->setDefault()
 474       * @param HTML_QuickForm_element|string $element
 475       * @param mixed $defaultvalue
 476       */
 477      public function set_element_default($element, $defaultvalue) {
 478          if ($element instanceof HTML_QuickForm_element) {
 479              $element = $element->getName();
 480          }
 481          $this->_form->setDefault($element, $defaultvalue);
 482      }
 483  
 484  
 485      /**
 486       * Sets the default value for form element - wrapper to $this->_form->setType()
 487       * @param HTML_QuickForm_element|string $element
 488       * @param int $type
 489       */
 490      public function set_element_type($element, $type) {
 491          if ($element instanceof HTML_QuickForm_element) {
 492              $element = $element->getName();
 493          }
 494          $this->_form->setType($element, $type);
 495      }
 496  
 497      /**
 498       * Adds a validation rule for the given field - wrapper for $this->_form->addRule()
 499       *
 500       * Do not use for 'required' rule!
 501       * Required * will be added automatically, if additional validation is needed
 502       * use method {@link self::add_validation_rule()}
 503       *
 504       * @param string $element Form element name
 505       * @param string $message Message to display for invalid data
 506       * @param string $type Rule type, use getRegisteredRules() to get types
 507       * @param string $format (optional)Required for extra rule data
 508       * @param string $validation (optional)Where to perform validation: "server", "client"
 509       * @param bool $reset Client-side validation: reset the form element to its original value if there is an error?
 510       * @param bool $force Force the rule to be applied, even if the target form element does not exist
 511       */
 512      public function add_element_rule($element, $message, $type, $format = null, $validation = 'server',
 513              $reset = false, $force = false) {
 514          if ($element instanceof HTML_QuickForm_element) {
 515              $element = $element->getName();
 516          }
 517          $this->_form->addRule($element, $message, $type, $format, $validation, $reset, $force);
 518      }
 519  
 520      /**
 521       * Adds a validation rule to the form
 522       *
 523       * @param callable $callback with arguments ($values, $files)
 524       */
 525      public function add_validation_rule(callable $callback) {
 526          if ($this->mode == self::MODE_COMPLETE) {
 527              $this->_form->addFormRule($callback);
 528          }
 529      }
 530  
 531      /**
 532       * Returns a reference to the element - wrapper for function $this->_form->getElement()
 533       *
 534       * @param string $elementname Element name
 535       * @return HTML_QuickForm_element reference to element
 536       */
 537      public function get_form_element($elementname) {
 538          return $this->_form->getElement($elementname);
 539      }
 540  
 541      /**
 542       * Displays the form
 543       */
 544      public function display() {
 545          global $OUTPUT, $PAGE;
 546          // Finalize the form definition if not yet done.
 547          if (!$this->_definition_finalized) {
 548              $this->_definition_finalized = true;
 549              $this->definition_after_data();
 550          }
 551  
 552          $mform = $this->_form;
 553  
 554          // Add "This form has required fields" text in the bottom of the form.
 555          if (($mform->_required || $this->hasrequired) &&
 556                 ($this->mode == self::MODE_COMPLETE || $this->mode == self::MODE_PRINT || $this->mode == self::MODE_VIEW_TEMPLATE)) {
 557              $element = $mform->addElement('static', 'requiredfields', '',
 558                      get_string('somefieldsrequired', 'form',
 559                              $OUTPUT->pix_icon('req', get_string('requiredelement', 'form'))));
 560              $element->setAttributes($element->getAttributes() + ['class' => 'requirednote']);
 561          }
 562  
 563          // Reset _required array so the default red * are not displayed.
 564          $mform->_required = array();
 565  
 566          // Move buttons to the end of the form.
 567          if ($this->mode == self::MODE_COMPLETE) {
 568              $mform->addElement('hidden', '__dummyelement');
 569              $buttons = $mform->removeElement('buttonar', false);
 570              $mform->insertElementBefore($buttons, '__dummyelement');
 571              $mform->removeElement('__dummyelement');
 572          }
 573  
 574          $this->_form->display();
 575  
 576          if ($this->mode == self::MODE_EDIT) {
 577              $PAGE->requires->js_call_amd('mod_feedback/edit', 'setup');
 578          }
 579      }
 580  }