Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.
   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   * This file contains the marking guide editor element
  19   *
  20   * @package    gradingform_guide
  21   * @copyright  2012 Dan Marsden <dan@danmarsden.com>
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  defined('MOODLE_INTERNAL') || die();
  26  
  27  require_once("HTML/QuickForm/input.php");
  28  
  29  /**
  30   * The editor for the marking guide advanced grading plugin.
  31   *
  32   * @package    gradingform_guide
  33   * @copyright  2012 Dan Marsden <dan@danmarsden.com>
  34   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  35   */
  36  class moodlequickform_guideeditor extends HTML_QuickForm_input {
  37      /** @var string help message */
  38      public $_helpbutton = '';
  39      /** @var null|false|string stores the result of the last validation: null - undefined, false - no errors,
  40       * string - error(s) text */
  41      protected $validationerrors = null;
  42      /** @var bool if element has already been validated **/
  43      protected $wasvalidated = false;
  44      /** @var null|bool If non-submit (JS) button was pressed: null - unknown, true/false - button was/wasn't pressed */
  45      protected $nonjsbuttonpressed = false;
  46      /** @var string|false Message to display in front of the editor (that there exist grades on this guide being edited) */
  47      protected $regradeconfirmation = false;
  48  
  49      /**
  50       * Constructor
  51       *
  52       * @param string $elementname
  53       * @param string $elementlabel
  54       * @param array $attributes
  55       */
  56      public function __construct($elementname=null, $elementlabel=null, $attributes=null) {
  57          parent::__construct($elementname, $elementlabel, $attributes);
  58      }
  59  
  60      /**
  61       * Old syntax of class constructor. Deprecated in PHP7.
  62       *
  63       * @deprecated since Moodle 3.1
  64       */
  65      public function moodlequickform_guideeditor($elementname=null, $elementlabel=null, $attributes=null) {
  66          debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER);
  67          self::__construct($elementname, $elementlabel, $attributes);
  68      }
  69  
  70      /**
  71       * get html for help button
  72       *
  73       * @return  string html for help button
  74       */
  75      public function getHelpButton() {
  76          return $this->_helpbutton;
  77      }
  78  
  79      /**
  80       * The renderer will take care itself about different display in normal and frozen states
  81       *
  82       * @return string
  83       */
  84      public function getElementTemplateType() {
  85          return 'default';
  86      }
  87  
  88      /**
  89       * Specifies that confirmation about re-grading needs to be added to this rubric editor.
  90       * $changelevel is saved in $this->regradeconfirmation and retrieved in toHtml()
  91       *
  92       * @see gradingform_rubric_controller::update_or_check_rubric()
  93       * @param int $changelevel
  94       */
  95      public function add_regrade_confirmation($changelevel) {
  96          $this->regradeconfirmation = $changelevel;
  97      }
  98  
  99      /**
 100       * Returns html string to display this element
 101       *
 102       * @return string
 103       */
 104      public function toHtml() {
 105          global $PAGE;
 106          $html = $this->_getTabs();
 107          $renderer = $PAGE->get_renderer('gradingform_guide');
 108          $data = $this->prepare_data(null, $this->wasvalidated);
 109          if (!$this->_flagFrozen) {
 110              $mode = gradingform_guide_controller::DISPLAY_EDIT_FULL;
 111              $module = array('name'=>'gradingform_guideeditor',
 112                  'fullpath'=>'/grade/grading/form/guide/js/guideeditor.js',
 113                  'requires' => array('base', 'dom', 'event', 'event-touch', 'escape'),
 114                  'strings' => array(
 115                      array('confirmdeletecriterion', 'gradingform_guide'),
 116                      array('clicktoedit', 'gradingform_guide'),
 117                      array('clicktoeditname', 'gradingform_guide')
 118              ));
 119              $PAGE->requires->js_init_call('M.gradingform_guideeditor.init', array(
 120                  array('name' => $this->getName(),
 121                      'criteriontemplate' => $renderer->criterion_template($mode, $data['options'], $this->getName()),
 122                      'commenttemplate' => $renderer->comment_template($mode, $this->getName())
 123                     )),
 124                  true, $module);
 125          } else {
 126              // Guide is frozen, no javascript needed.
 127              if ($this->_persistantFreeze) {
 128                  $mode = gradingform_guide_controller::DISPLAY_EDIT_FROZEN;
 129              } else {
 130                  $mode = gradingform_guide_controller::DISPLAY_PREVIEW;
 131              }
 132          }
 133          if ($this->regradeconfirmation) {
 134              if (!isset($data['regrade'])) {
 135                  $data['regrade'] = 1;
 136              }
 137              $html .= $renderer->display_regrade_confirmation($this->getName(), $this->regradeconfirmation, $data['regrade']);
 138          }
 139          if ($this->validationerrors) {
 140              $html .= html_writer::div($renderer->notification($this->validationerrors));
 141          }
 142          $html .= $renderer->display_guide($data['criteria'], $data['comments'], $data['options'], $mode, $this->getName());
 143          return $html;
 144      }
 145      /**
 146       * Prepares the data passed in $_POST:
 147       * - processes the pressed buttons 'addlevel', 'addcriterion', 'moveup', 'movedown', 'delete' (when JavaScript is disabled)
 148       *   sets $this->nonjsbuttonpressed to true/false if such button was pressed
 149       * - if options not passed (i.e. we create a new guide) fills the options array with the default values
 150       * - if options are passed completes the options array with unchecked checkboxes
 151       * - if $withvalidation is set, adds 'error_xxx' attributes to elements that contain errors and creates an error string
 152       *   and stores it in $this->validationerrors
 153       *
 154       * @param array $value
 155       * @param boolean $withvalidation whether to enable data validation
 156       * @return array
 157       */
 158      protected function prepare_data($value = null, $withvalidation = false) {
 159          if (null === $value) {
 160              $value = $this->getValue();
 161          }
 162          if ($this->nonjsbuttonpressed === null) {
 163              $this->nonjsbuttonpressed = false;
 164          }
 165  
 166          $errors = array();
 167          $return = array('criteria' => array(), 'options' => gradingform_guide_controller::get_default_options(),
 168              'comments' => array());
 169          if (!isset($value['criteria'])) {
 170              $value['criteria'] = array();
 171              $errors['err_nocriteria'] = 1;
 172          }
 173          // If options are present in $value, replace default values with submitted values.
 174          if (!empty($value['options'])) {
 175              foreach (array_keys($return['options']) as $option) {
 176                  // Special treatment for checkboxes.
 177                  if (!empty($value['options'][$option])) {
 178                      $return['options'][$option] = $value['options'][$option];
 179                  } else {
 180                      $return['options'][$option] = null;
 181                  }
 182  
 183              }
 184          }
 185  
 186          if (is_array($value)) {
 187              // For other array keys of $value no special treatmeant neeeded, copy them to return value as is.
 188              foreach (array_keys($value) as $key) {
 189                  if ($key != 'options' && $key != 'criteria' && $key != 'comments') {
 190                      $return[$key] = $value[$key];
 191                  }
 192              }
 193          }
 194  
 195          // Iterate through criteria.
 196          $lastaction = null;
 197          $lastid = null;
 198          foreach ($value['criteria'] as $id => $criterion) {
 199              if ($id == 'addcriterion') {
 200                  $id = $this->get_next_id(array_keys($value['criteria']));
 201                  $criterion = array('description' => '');
 202                  $this->nonjsbuttonpressed = true;
 203              }
 204  
 205              if ($withvalidation && !array_key_exists('delete', $criterion)) {
 206                  if (!strlen(trim($criterion['shortname']))) {
 207                      $errors['err_noshortname'] = 1;
 208                      $criterion['error_description'] = true;
 209                  }
 210                  if (strlen(trim($criterion['shortname'])) > 255) {
 211                      $errors['err_shortnametoolong'] = 1;
 212                      $criterion['error_description'] = true;
 213                  }
 214                  if (!strlen(trim($criterion['maxscore']))) {
 215                      $errors['err_nomaxscore'] = 1;
 216                      $criterion['error_description'] = true;
 217                  } else if (!is_numeric($criterion['maxscore'])) {
 218                      $errors['err_maxscorenotnumeric'] = 1;
 219                      $criterion['error_description'] = true;
 220                  } else if ($criterion['maxscore'] < 0) {
 221                      $errors['err_maxscoreisnegative'] = 1;
 222                      $criterion['error_description'] = true;
 223                  }
 224              }
 225              if (array_key_exists('moveup', $criterion) || $lastaction == 'movedown') {
 226                  unset($criterion['moveup']);
 227                  if ($lastid !== null) {
 228                      $lastcriterion = $return['criteria'][$lastid];
 229                      unset($return['criteria'][$lastid]);
 230                      $return['criteria'][$id] = $criterion;
 231                      $return['criteria'][$lastid] = $lastcriterion;
 232                  } else {
 233                      $return['criteria'][$id] = $criterion;
 234                  }
 235                  $lastaction = null;
 236                  $lastid = $id;
 237                  $this->nonjsbuttonpressed = true;
 238              } else if (array_key_exists('delete', $criterion)) {
 239                  $this->nonjsbuttonpressed = true;
 240              } else {
 241                  if (array_key_exists('movedown', $criterion)) {
 242                      unset($criterion['movedown']);
 243                      $lastaction = 'movedown';
 244                      $this->nonjsbuttonpressed = true;
 245                  }
 246                  $return['criteria'][$id] = $criterion;
 247                  $lastid = $id;
 248              }
 249          }
 250  
 251          // Add sort order field to criteria.
 252          $csortorder = 1;
 253          foreach (array_keys($return['criteria']) as $id) {
 254              $return['criteria'][$id]['sortorder'] = $csortorder++;
 255          }
 256  
 257          // Iterate through comments.
 258          $lastaction = null;
 259          $lastid = null;
 260          if (!empty($value['comments'])) {
 261              foreach ($value['comments'] as $id => $comment) {
 262                  if ($id == 'addcomment') {
 263                      $id = $this->get_next_id(array_keys($value['comments']));
 264                      $comment = array('description' => '');
 265                      $this->nonjsbuttonpressed = true;
 266                  }
 267  
 268                  if (array_key_exists('moveup', $comment) || $lastaction == 'movedown') {
 269                      unset($comment['moveup']);
 270                      if ($lastid !== null) {
 271                          $lastcomment = $return['comments'][$lastid];
 272                          unset($return['comments'][$lastid]);
 273                          $return['comments'][$id] = $comment;
 274                          $return['comments'][$lastid] = $lastcomment;
 275                      } else {
 276                          $return['comments'][$id] = $comment;
 277                      }
 278                      $lastaction = null;
 279                      $lastid = $id;
 280                      $this->nonjsbuttonpressed = true;
 281                  } else if (array_key_exists('delete', $comment)) {
 282                      $this->nonjsbuttonpressed = true;
 283                  } else {
 284                      if (array_key_exists('movedown', $comment)) {
 285                          unset($comment['movedown']);
 286                          $lastaction = 'movedown';
 287                          $this->nonjsbuttonpressed = true;
 288                      }
 289                      $return['comments'][$id] = $comment;
 290                      $lastid = $id;
 291                  }
 292              }
 293              // Add sort order field to comments.
 294              $csortorder = 1;
 295              foreach (array_keys($return['comments']) as $id) {
 296                  $return['comments'][$id]['sortorder'] = $csortorder++;
 297              }
 298          }
 299          // Create validation error string (if needed).
 300          if ($withvalidation) {
 301              if (count($errors)) {
 302                  $rv = array();
 303                  foreach ($errors as $error => $v) {
 304                      $rv[] = get_string($error, 'gradingform_guide');
 305                  }
 306                  $this->validationerrors = join('<br/ >', $rv);
 307              } else {
 308                  $this->validationerrors = false;
 309              }
 310              $this->wasvalidated = true;
 311          }
 312          return $return;
 313  
 314      }
 315  
 316      /**
 317       * Scans array $ids to find the biggest element ! NEWID*, increments it by 1 and returns
 318       *
 319       * @param array $ids
 320       * @return string
 321       */
 322      protected function get_next_id($ids) {
 323          $maxid = 0;
 324          foreach ($ids as $id) {
 325              if (preg_match('/^NEWID(\d+)$/', $id, $matches) && ((int)$matches[1]) > $maxid) {
 326                  $maxid = (int)$matches[1];
 327              }
 328          }
 329          return 'NEWID'.($maxid+1);
 330      }
 331  
 332      /**
 333       * Checks if a submit button was pressed which is supposed to be processed on client side by JS
 334       * but user seem to have disabled JS in the browser.
 335       * (buttons 'add criteria', 'add level', 'move up', 'move down', 'add comment')
 336       * In this case the form containing this element is prevented from being submitted
 337       *
 338       * @param array $value
 339       * @return boolean true if non-submit button was pressed and not processed by JS
 340       */
 341      public function non_js_button_pressed($value) {
 342          if ($this->nonjsbuttonpressed === null) {
 343              $this->prepare_data($value);
 344          }
 345          return $this->nonjsbuttonpressed;
 346      }
 347  
 348      /**
 349       * Validates that guide has at least one criterion, filled definitions and all criteria
 350       * have filled descriptions
 351       *
 352       * @param array $value
 353       * @return string|false error text or false if no errors found
 354       */
 355      public function validate($value) {
 356          if (!$this->wasvalidated) {
 357              $this->prepare_data($value, true);
 358          }
 359          return $this->validationerrors;
 360      }
 361  
 362      /**
 363       * Prepares the data for saving
 364       * @see prepare_data()
 365       *
 366       * @param array $submitvalues
 367       * @param boolean $assoc
 368       * @return array
 369       */
 370      public function exportValue(&$submitvalues, $assoc = false) {
 371          $value =  $this->prepare_data($this->_findValue($submitvalues));
 372          return $this->_prepareValue($value, $assoc);
 373      }
 374  }