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.
   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   * File contains definition of class MoodleQuickForm_rubriceditor
  19   *
  20   * @package    gradingform_rubric
  21   * @copyright  2011 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  require_once("HTML/QuickForm/input.php");
  28  
  29  /**
  30   * Form element for handling rubric editor
  31   *
  32   * The rubric editor is defined as a separate form element. This allows us to render
  33   * criteria, levels and buttons using the rubric's own renderer. Also, the required
  34   * Javascript library is included, which processes, on the client, buttons needed
  35   * for reordering, adding and deleting criteria.
  36   *
  37   * If Javascript is disabled when one of those special buttons is pressed, the form
  38   * element is not validated and, instead of submitting the form, we process button presses.
  39   *
  40   * @package    gradingform_rubric
  41   * @copyright  2011 Marina Glancy
  42   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  43   */
  44  class MoodleQuickForm_rubriceditor extends HTML_QuickForm_input {
  45      /** @var string help message */
  46      public $_helpbutton = '';
  47      /** @var string|bool stores the result of the last validation: null - undefined, false - no errors, string - error(s) text */
  48      protected $validationerrors = null;
  49      /** @var bool if element has already been validated **/
  50      protected $wasvalidated = false;
  51      /** @var bool If non-submit (JS) button was pressed: null - unknown, true/false - button was/wasn't pressed */
  52      protected $nonjsbuttonpressed = false;
  53      /** @var bool Message to display in front of the editor (that there exist grades on this rubric being edited) */
  54      protected $regradeconfirmation = false;
  55  
  56      /**
  57       * Constructor for rubric editor
  58       *
  59       * @param string $elementName
  60       * @param string $elementLabel
  61       * @param array $attributes
  62       */
  63      public function __construct($elementName=null, $elementLabel=null, $attributes=null) {
  64          parent::__construct($elementName, $elementLabel, $attributes);
  65      }
  66  
  67      /**
  68       * Old syntax of class constructor. Deprecated in PHP7.
  69       *
  70       * @deprecated since Moodle 3.1
  71       */
  72      public function MoodleQuickForm_rubriceditor($elementName=null, $elementLabel=null, $attributes=null) {
  73          debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER);
  74          self::__construct($elementName, $elementLabel, $attributes);
  75      }
  76  
  77      /**
  78       * get html for help button
  79       *
  80       * @return string html for help button
  81       */
  82      public function getHelpButton() {
  83          return $this->_helpbutton;
  84      }
  85  
  86      /**
  87       * The renderer will take care itself about different display in normal and frozen states
  88       *
  89       * @return string
  90       */
  91      public function getElementTemplateType() {
  92          return 'default';
  93      }
  94  
  95      /**
  96       * Specifies that confirmation about re-grading needs to be added to this rubric editor.
  97       * $changelevel is saved in $this->regradeconfirmation and retrieved in toHtml()
  98       *
  99       * @see gradingform_rubric_controller::update_or_check_rubric()
 100       * @param int $changelevel
 101       */
 102      public function add_regrade_confirmation($changelevel) {
 103          $this->regradeconfirmation = $changelevel;
 104      }
 105  
 106      /**
 107       * Returns html string to display this element
 108       *
 109       * @return string
 110       */
 111      public function toHtml() {
 112          global $PAGE;
 113          $html = $this->_getTabs();
 114          $renderer = $PAGE->get_renderer('gradingform_rubric');
 115          $data = $this->prepare_data(null, $this->wasvalidated);
 116          if (!$this->_flagFrozen) {
 117              $mode = gradingform_rubric_controller::DISPLAY_EDIT_FULL;
 118              $module = array('name'=>'gradingform_rubriceditor', 'fullpath'=>'/grade/grading/form/rubric/js/rubriceditor.js',
 119                  'requires' => array('base', 'dom', 'event', 'event-touch', 'escape'),
 120                  'strings' => array(array('confirmdeletecriterion', 'gradingform_rubric'), array('confirmdeletelevel', 'gradingform_rubric'),
 121                      array('criterionempty', 'gradingform_rubric'), array('levelempty', 'gradingform_rubric')
 122                  ));
 123              $PAGE->requires->js_init_call('M.gradingform_rubriceditor.init', array(
 124                  array('name' => $this->getName(),
 125                      'criteriontemplate' => $renderer->criterion_template($mode, $data['options'], $this->getName()),
 126                      'leveltemplate' => $renderer->level_template($mode, $data['options'], $this->getName())
 127                     )),
 128                  true, $module);
 129          } else {
 130              // Rubric is frozen, no javascript needed
 131              if ($this->_persistantFreeze) {
 132                  $mode = gradingform_rubric_controller::DISPLAY_EDIT_FROZEN;
 133              } else {
 134                  $mode = gradingform_rubric_controller::DISPLAY_PREVIEW;
 135              }
 136          }
 137          if ($this->regradeconfirmation) {
 138              if (!isset($data['regrade'])) {
 139                  $data['regrade'] = 1;
 140              }
 141              $html .= $renderer->display_regrade_confirmation($this->getName(), $this->regradeconfirmation, $data['regrade']);
 142          }
 143          if ($this->validationerrors) {
 144              $html .= html_writer::div($renderer->notification($this->validationerrors));
 145          }
 146          $html .= $renderer->display_rubric($data['criteria'], $data['options'], $mode, $this->getName());
 147          return $html;
 148      }
 149  
 150      /**
 151       * Prepares the data passed in $_POST:
 152       * - processes the pressed buttons 'addlevel', 'addcriterion', 'moveup', 'movedown', 'delete' (when JavaScript is disabled)
 153       *   sets $this->nonjsbuttonpressed to true/false if such button was pressed
 154       * - if options not passed (i.e. we create a new rubric) fills the options array with the default values
 155       * - if options are passed completes the options array with unchecked checkboxes
 156       * - if $withvalidation is set, adds 'error_xxx' attributes to elements that contain errors and creates an error string
 157       *   and stores it in $this->validationerrors
 158       *
 159       * @param array $value
 160       * @param boolean $withvalidation whether to enable data validation
 161       * @return array
 162       */
 163      protected function prepare_data($value = null, $withvalidation = false) {
 164          if (null === $value) {
 165              $value = $this->getValue();
 166          }
 167          if ($this->nonjsbuttonpressed === null) {
 168              $this->nonjsbuttonpressed = false;
 169          }
 170          $totalscore = 0;
 171          $errors = array();
 172          $return = array('criteria' => array(), 'options' => gradingform_rubric_controller::get_default_options());
 173          if (!isset($value['criteria'])) {
 174              $value['criteria'] = array();
 175              $errors['err_nocriteria'] = 1;
 176          }
 177          // If options are present in $value, replace default values with submitted values
 178          if (!empty($value['options'])) {
 179              foreach (array_keys($return['options']) as $option) {
 180                  // special treatment for checkboxes
 181                  if (!empty($value['options'][$option])) {
 182                      $return['options'][$option] = $value['options'][$option];
 183                  } else {
 184                      $return['options'][$option] = null;
 185                  }
 186              }
 187          }
 188          if (is_array($value)) {
 189              // for other array keys of $value no special treatmeant neeeded, copy them to return value as is
 190              foreach (array_keys($value) as $key) {
 191                  if ($key != 'options' && $key != 'criteria') {
 192                      $return[$key] = $value[$key];
 193                  }
 194              }
 195          }
 196  
 197          // iterate through criteria
 198          $lastaction = null;
 199          $lastid = null;
 200          $overallminscore = $overallmaxscore = 0;
 201          foreach ($value['criteria'] as $id => $criterion) {
 202              if ($id == 'addcriterion') {
 203                  $id = $this->get_next_id(array_keys($value['criteria']));
 204                  $criterion = array('description' => '', 'levels' => array());
 205                  $i = 0;
 206                  // when adding new criterion copy the number of levels and their scores from the last criterion
 207                  if (!empty($value['criteria'][$lastid]['levels'])) {
 208                      foreach ($value['criteria'][$lastid]['levels'] as $lastlevel) {
 209                          $criterion['levels']['NEWID'.($i++)]['score'] = $lastlevel['score'];
 210                      }
 211                  } else {
 212                      $criterion['levels']['NEWID'.($i++)]['score'] = 0;
 213                  }
 214                  // add more levels so there are at least 3 in the new criterion. Increment by 1 the score for each next one
 215                  for ($i=$i; $i<3; $i++) {
 216                      $criterion['levels']['NEWID'.$i]['score'] = $criterion['levels']['NEWID'.($i-1)]['score'] + 1;
 217                  }
 218                  // set other necessary fields (definition) for the levels in the new criterion
 219                  foreach (array_keys($criterion['levels']) as $i) {
 220                      $criterion['levels'][$i]['definition'] = '';
 221                  }
 222                  $this->nonjsbuttonpressed = true;
 223              }
 224              $levels = array();
 225              $minscore = $maxscore = null;
 226              if (array_key_exists('levels', $criterion)) {
 227                  foreach ($criterion['levels'] as $levelid => $level) {
 228                      if ($levelid == 'addlevel') {
 229                          $levelid = $this->get_next_id(array_keys($criterion['levels']));
 230                          $level = array(
 231                              'definition' => '',
 232                              'score' => 0,
 233                          );
 234                          foreach ($criterion['levels'] as $lastlevel) {
 235                              if (isset($lastlevel['score'])) {
 236                                  $level['score'] = max($level['score'], ceil(unformat_float($lastlevel['score'])) + 1);
 237                              }
 238                          }
 239                          $this->nonjsbuttonpressed = true;
 240                      }
 241                      if (!array_key_exists('delete', $level)) {
 242                          $score = unformat_float($level['score'], true);
 243                          if ($withvalidation) {
 244                              if (!strlen(trim($level['definition']))) {
 245                                  $errors['err_nodefinition'] = 1;
 246                                  $level['error_definition'] = true;
 247                              }
 248                              if ($score === null || $score === false) {
 249                                  $errors['err_scoreformat'] = 1;
 250                                  $level['error_score'] = true;
 251                              }
 252                          }
 253                          $levels[$levelid] = $level;
 254                          if ($minscore === null || $score < $minscore) {
 255                              $minscore = $score;
 256                          }
 257                          if ($maxscore === null || $score > $maxscore) {
 258                              $maxscore = $score;
 259                          }
 260                      } else {
 261                          $this->nonjsbuttonpressed = true;
 262                      }
 263                  }
 264              }
 265              $totalscore += (float)$maxscore;
 266              $criterion['levels'] = $levels;
 267              if ($withvalidation && !array_key_exists('delete', $criterion)) {
 268                  if (count($levels)<2) {
 269                      $errors['err_mintwolevels'] = 1;
 270                      $criterion['error_levels'] = true;
 271                  }
 272                  if (!strlen(trim($criterion['description']))) {
 273                      $errors['err_nodescription'] = 1;
 274                      $criterion['error_description'] = true;
 275                  }
 276                  $overallmaxscore += $maxscore;
 277                  $overallminscore += $minscore;
 278              }
 279              if (array_key_exists('moveup', $criterion) || $lastaction == 'movedown') {
 280                  unset($criterion['moveup']);
 281                  if ($lastid !== null) {
 282                      $lastcriterion = $return['criteria'][$lastid];
 283                      unset($return['criteria'][$lastid]);
 284                      $return['criteria'][$id] = $criterion;
 285                      $return['criteria'][$lastid] = $lastcriterion;
 286                  } else {
 287                      $return['criteria'][$id] = $criterion;
 288                  }
 289                  $lastaction = null;
 290                  $lastid = $id;
 291                  $this->nonjsbuttonpressed = true;
 292              } else if (array_key_exists('delete', $criterion)) {
 293                  $this->nonjsbuttonpressed = true;
 294              } else {
 295                  if (array_key_exists('movedown', $criterion)) {
 296                      unset($criterion['movedown']);
 297                      $lastaction = 'movedown';
 298                      $this->nonjsbuttonpressed = true;
 299                  }
 300                  $return['criteria'][$id] = $criterion;
 301                  $lastid = $id;
 302              }
 303          }
 304  
 305          if ($totalscore <= 0) {
 306              $errors['err_totalscore'] = 1;
 307          }
 308  
 309          // add sort order field to criteria
 310          $csortorder = 1;
 311          foreach (array_keys($return['criteria']) as $id) {
 312              $return['criteria'][$id]['sortorder'] = $csortorder++;
 313          }
 314  
 315          // create validation error string (if needed)
 316          if ($withvalidation) {
 317              if (!$return['options']['lockzeropoints']) {
 318                  if ($overallminscore == $overallmaxscore) {
 319                      $errors['err_novariations'] = 1;
 320                  }
 321              }
 322              if (count($errors)) {
 323                  $rv = array();
 324                  foreach ($errors as $error => $v) {
 325                      $rv[] = get_string($error, 'gradingform_rubric');
 326                  }
 327                  $this->validationerrors = join('<br/ >', $rv);
 328              } else {
 329                  $this->validationerrors = false;
 330              }
 331              $this->wasvalidated = true;
 332          }
 333          return $return;
 334      }
 335  
 336      /**
 337       * Scans array $ids to find the biggest element ! NEWID*, increments it by 1 and returns
 338       *
 339       * @param array $ids
 340       * @return string
 341       */
 342      protected function get_next_id($ids) {
 343          $maxid = 0;
 344          foreach ($ids as $id) {
 345              if (preg_match('/^NEWID(\d+)$/', $id, $matches) && ((int)$matches[1]) > $maxid) {
 346                  $maxid = (int)$matches[1];
 347              }
 348          }
 349          return 'NEWID'.($maxid+1);
 350      }
 351  
 352      /**
 353       * Checks if a submit button was pressed which is supposed to be processed on client side by JS
 354       * but user seem to have disabled JS in the browser.
 355       * (buttons 'add criteria', 'add level', 'move up', 'move down', etc.)
 356       * In this case the form containing this element is prevented from being submitted
 357       *
 358       * @param array $value
 359       * @return boolean true if non-submit button was pressed and not processed by JS
 360       */
 361      public function non_js_button_pressed($value) {
 362          if ($this->nonjsbuttonpressed === null) {
 363              $this->prepare_data($value);
 364          }
 365          return $this->nonjsbuttonpressed;
 366      }
 367  
 368      /**
 369       * Validates that rubric has at least one criterion, at least two levels within one criterion,
 370       * each level has a valid score, all levels have filled definitions and all criteria
 371       * have filled descriptions
 372       *
 373       * @param array $value
 374       * @return string|false error text or false if no errors found
 375       */
 376      public function validate($value) {
 377          if (!$this->wasvalidated) {
 378              $this->prepare_data($value, true);
 379          }
 380          return $this->validationerrors;
 381      }
 382  
 383      /**
 384       * Prepares the data for saving
 385       *
 386       * @see prepare_data()
 387       * @param array $submitValues
 388       * @param boolean $assoc
 389       * @return array
 390       */
 391      public function exportValue(&$submitValues, $assoc = false) {
 392          $value =  $this->prepare_data($this->_findValue($submitValues));
 393          return $this->_prepareValue($value, $assoc);
 394      }
 395  }