Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.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  namespace core_grades\form;
  18  
  19  defined('MOODLE_INTERNAL') || die;
  20  
  21  use context;
  22  use context_course;
  23  use core_form\dynamic_form;
  24  use grade_category;
  25  use grade_item;
  26  use grade_outcome;
  27  use grade_plugin_return;
  28  use moodle_url;
  29  
  30  require_once($CFG->dirroot.'/grade/lib.php');
  31  
  32  /**
  33   * Prints the add outcome gradebook form.
  34   *
  35   * @copyright 2023 Mathew May <mathew.solutions>
  36   * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
  37   * @package core_grades
  38   */
  39  class add_outcome extends dynamic_form {
  40  
  41      /** Grade plugin return tracking object.
  42       * @var object $gpr
  43       */
  44      public $gpr;
  45  
  46      /**
  47       * Helper function to grab the current grade outcome item based on information within the form.
  48       *
  49       * @return array
  50       * @throws \moodle_exception
  51       */
  52      private function get_gradeitem(): array {
  53          $courseid = $this->optional_param('courseid', null, PARAM_INT);
  54          $id = $this->optional_param('itemid', null, PARAM_INT);
  55  
  56          if ($gradeitem = grade_item::fetch(['id' => $id, 'courseid' => $courseid])) {
  57              // Redirect if outcomeid not present.
  58              if (empty($gradeitem->outcomeid)) {
  59                  $url = new moodle_url('/grade/edit/tree/item.php', ['id' => $id, 'courseid' => $courseid]);
  60                  redirect($this->gpr->add_url_params($url));
  61              }
  62              $item = $gradeitem->get_record_data();
  63              $parentcategory = $gradeitem->get_parent_category();
  64              if ($item->itemtype == 'mod') {
  65                  $cm = get_coursemodule_from_instance($item->itemmodule, $item->iteminstance, $item->courseid);
  66                  $item->cmid = $cm->id;
  67              } else {
  68                  $item->cmid = 0;
  69              }
  70          } else {
  71              $gradeitem = new grade_item(['courseid' => $courseid, 'itemtype' => 'manual'], false);
  72              $item = $gradeitem->get_record_data();
  73              $parentcategory = grade_category::fetch_course_category($courseid);
  74          }
  75          $item->parentcategory = $parentcategory->id;
  76  
  77          if ($item->hidden > 1) {
  78              $item->hiddenuntil = $item->hidden;
  79              $item->hidden = 0;
  80          } else {
  81              $item->hiddenuntil = 0;
  82          }
  83  
  84          $item->locked = !empty($item->locked);
  85  
  86          if ($parentcategory->aggregation == GRADE_AGGREGATE_SUM || $parentcategory->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN2) {
  87              $item->aggregationcoef = $item->aggregationcoef == 0 ? 0 : 1;
  88          } else {
  89              $item->aggregationcoef = format_float($item->aggregationcoef, 4);
  90          }
  91          if ($parentcategory->aggregation == GRADE_AGGREGATE_SUM) {
  92              $item->aggregationcoef2 = format_float($item->aggregationcoef2 * 100.0);
  93          }
  94          $item->cancontrolvisibility = $gradeitem->can_control_visibility();
  95          return [
  96              'gradeitem' => $gradeitem,
  97              'item' => $item
  98          ];
  99      }
 100  
 101      /**
 102       * Form definition
 103       *
 104       * @return void
 105       * @throws \coding_exception
 106       * @throws \dml_exception
 107       * @throws \moodle_exception
 108       */
 109      protected function definition() {
 110          $courseid = $this->optional_param('courseid', null, PARAM_INT);
 111          $id = $this->optional_param('itemid', 0, PARAM_INT);
 112          $gprplugin = $this->optional_param('gpr_plugin', '', PARAM_TEXT);
 113  
 114          if ($gprplugin && ($gprplugin !== 'tree')) {
 115              $this->gpr = new grade_plugin_return(['type' => 'report', 'plugin' => $gprplugin, 'courseid' => $courseid]);
 116          } else {
 117              $this->gpr = new grade_plugin_return(['type' => 'edit', 'plugin' => 'tree', 'courseid' => $courseid]);
 118          }
 119  
 120          $mform =& $this->_form;
 121  
 122          $local = $this->get_gradeitem();
 123          $gradeitem = $local['gradeitem'];
 124          $item = $local['item'];
 125  
 126          // Hidden elements.
 127          $mform->addElement('hidden', 'id', 0);
 128          $mform->setType('id', PARAM_INT);
 129          $mform->addElement('hidden', 'courseid', $courseid);
 130          $mform->setType('courseid', PARAM_INT);
 131          $mform->addElement('hidden', 'itemid', $id);
 132          $mform->setType('itemid', PARAM_INT);
 133  
 134          // Allow setting of outcomes on module items too.
 135          $outcomeoptions = [];
 136          if ($outcomes = grade_outcome::fetch_all_available($courseid)) {
 137              foreach ($outcomes as $outcome) {
 138                  $outcomeoptions[$outcome->id] = $outcome->get_name();
 139              }
 140          }
 141  
 142          // Visible elements.
 143          $mform->addElement('text', 'itemname', get_string('itemname', 'grades'));
 144          $mform->addRule('itemname', get_string('required'), 'required', null, 'client');
 145          $mform->setType('itemname', PARAM_TEXT);
 146  
 147          $mform->addElement('selectwithlink', 'outcomeid', get_string('outcome', 'grades'), $outcomeoptions);
 148          $mform->addHelpButton('outcomeid', 'outcome', 'grades');
 149          $mform->addRule('outcomeid', get_string('required'), 'required');
 150  
 151          $options = [0 => get_string('none')];
 152          if ($coursemods = get_course_mods($courseid)) {
 153              foreach ($coursemods as $coursemod) {
 154                  if ($mod = get_coursemodule_from_id($coursemod->modname, $coursemod->id)) {
 155                      $options[$coursemod->id] = format_string($mod->name);
 156                  }
 157              }
 158          }
 159          $mform->addElement('select', 'cmid', get_string('linkedactivity', 'grades'), $options);
 160          $mform->addHelpButton('cmid', 'linkedactivity', 'grades');
 161          $mform->setDefault('cmid', 0);
 162  
 163          // Hiding.
 164          $mform->addElement('checkbox', 'hidden', get_string('hidden', 'grades'));
 165          $mform->addHelpButton('hidden', 'hidden', 'grades');
 166  
 167          // Locking.
 168          $mform->addElement('advcheckbox', 'locked', get_string('locked', 'grades'));
 169          $mform->addHelpButton('locked', 'locked', 'grades');
 170  
 171          // Parent category related settings.
 172          $mform->addElement('advcheckbox', 'weightoverride', get_string('adjustedweight', 'grades'));
 173          $mform->addHelpButton('weightoverride', 'weightoverride', 'grades');
 174  
 175          $mform->addElement('text', 'aggregationcoef2', get_string('weight', 'grades'));
 176          $mform->addHelpButton('aggregationcoef2', 'weight', 'grades');
 177          $mform->setType('aggregationcoef2', PARAM_RAW);
 178          $mform->hideIf('aggregationcoef2', 'weightoverride');
 179  
 180          $options = [];
 181          $coefstring = '';
 182          $categories = grade_category::fetch_all(['courseid' => $courseid]);
 183          foreach ($categories as $cat) {
 184              $cat->apply_forced_settings();
 185              $options[$cat->id] = $cat->get_name();
 186              if ($cat->is_aggregationcoef_used()) {
 187                  if ($cat->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN) {
 188                      $coefstring = ($coefstring == '' || $coefstring == 'aggregationcoefweight') ?
 189                          'aggregationcoefweight' : 'aggregationcoef';
 190                  } else if ($cat->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN2) {
 191                      $coefstring = ($coefstring == '' || $coefstring == 'aggregationcoefextrasum') ?
 192                          'aggregationcoefextrasum' : 'aggregationcoef';
 193                  } else if ($cat->aggregation == GRADE_AGGREGATE_EXTRACREDIT_MEAN) {
 194                      $coefstring = ($coefstring == '' || $coefstring == 'aggregationcoefextraweight') ?
 195                          'aggregationcoefextraweight' : 'aggregationcoef';
 196                  } else if ($cat->aggregation == GRADE_AGGREGATE_SUM) {
 197                      $coefstring = ($coefstring == '' || $coefstring == 'aggregationcoefextrasum') ?
 198                          'aggregationcoefextrasum' : 'aggregationcoef';
 199                  } else {
 200                      $coefstring = 'aggregationcoef';
 201                  }
 202              } else {
 203                  $mform->disabledIf('aggregationcoef', 'parentcategory', 'eq', $cat->id);
 204              }
 205          }
 206  
 207          if (count($categories) > 1) {
 208              $mform->addElement('select', 'parentcategory', get_string('gradecategory', 'grades'), $options);
 209              $mform->disabledIf('parentcategory', 'cmid', 'noteq', 0);
 210          }
 211  
 212          if ($coefstring !== '') {
 213              if ($coefstring == 'aggregationcoefextrasum' || $coefstring == 'aggregationcoefextraweightsum') {
 214                  $coefstring = 'aggregationcoefextrasum';
 215                  $mform->addElement('checkbox', 'aggregationcoef', get_string($coefstring, 'grades'));
 216              } else {
 217                  $mform->addElement('text', 'aggregationcoef', get_string($coefstring, 'grades'));
 218              }
 219              $mform->addHelpButton('aggregationcoef', $coefstring, 'grades');
 220          }
 221  
 222          // Remove the aggregation coef element if not needed.
 223          if ($gradeitem->is_course_item()) {
 224              if ($mform->elementExists('parentcategory')) {
 225                  $mform->removeElement('parentcategory');
 226              }
 227              if ($mform->elementExists('aggregationcoef')) {
 228                  $mform->removeElement('aggregationcoef');
 229              }
 230  
 231          } else {
 232              // If we wanted to change parent of existing item - we would have to verify there are no circular references in parents.
 233              if ($id > -1 && $mform->elementExists('parentcategory')) {
 234                  $mform->hardFreeze('parentcategory');
 235              }
 236  
 237              $parentcategory = $gradeitem->get_parent_category();
 238              if (!$parentcategory) {
 239                  // If we do not have an id, we are creating a new grade item.
 240  
 241                  // Assign the course category to this grade item.
 242                  $parentcategory = grade_category::fetch_course_category($courseid);
 243                  $gradeitem->parent_category = $parentcategory;
 244              }
 245  
 246              $parentcategory->apply_forced_settings();
 247  
 248              if (!$parentcategory->is_aggregationcoef_used() || !$parentcategory->aggregateoutcomes) {
 249                  if ($mform->elementExists('aggregationcoef')) {
 250                      $mform->removeElement('aggregationcoef');
 251                  }
 252              } else {
 253                  // Fix label if needed.
 254                  $agg_el =& $mform->getElement('aggregationcoef');
 255                  $aggcoef = '';
 256                  if ($parentcategory->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN) {
 257                      $aggcoef = 'aggregationcoefweight';
 258  
 259                  } else if ($parentcategory->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN2) {
 260                      $aggcoef = 'aggregationcoefextrasum';
 261  
 262                  } else if ($parentcategory->aggregation == GRADE_AGGREGATE_EXTRACREDIT_MEAN) {
 263                      $aggcoef = 'aggregationcoefextraweight';
 264  
 265                  } else if ($parentcategory->aggregation == GRADE_AGGREGATE_SUM) {
 266                      $aggcoef = 'aggregationcoefextrasum';
 267                  }
 268  
 269                  if ($aggcoef !== '') {
 270                      $agg_el->setLabel(get_string($aggcoef, 'grades'));
 271                      $mform->addHelpButton('aggregationcoef', $aggcoef, 'grades');
 272                  }
 273              }
 274  
 275              // Remove the natural weighting fields for other aggregations,
 276              // or when the category does not aggregate outcomes.
 277              if ($parentcategory->aggregation != GRADE_AGGREGATE_SUM ||
 278                  !$parentcategory->aggregateoutcomes) {
 279                  if ($mform->elementExists('weightoverride')) {
 280                      $mform->removeElement('weightoverride');
 281                  }
 282                  if ($mform->elementExists('aggregationcoef2')) {
 283                      $mform->removeElement('aggregationcoef2');
 284                  }
 285              }
 286          }
 287  
 288          $url = new moodle_url('/grade/edit/tree/outcomeitem.php', ['id' => $id, 'courseid' => $courseid]);
 289          $url = $this->gpr->add_url_params($url);
 290          $url = '<a class="showadvancedform" href="' . $url . '">' . get_string('showmore', 'form') .'</a>';
 291          $mform->addElement('static', 'advancedform', $url);
 292  
 293          // Add return tracking info.
 294          $this->gpr->add_mform_elements($mform);
 295  
 296          $this->set_data($item);
 297      }
 298  
 299      /**
 300       * Return form context
 301       *
 302       * @return context
 303       */
 304      protected function get_context_for_dynamic_submission(): context {
 305          $courseid = $this->optional_param('courseid', null, PARAM_INT);
 306          return context_course::instance($courseid);
 307      }
 308  
 309      /**
 310       * Check if current user has access to this form, otherwise throw exception
 311       *
 312       * @return void
 313       * @throws \required_capability_exception
 314       */
 315      protected function check_access_for_dynamic_submission(): void {
 316          $courseid = $this->optional_param('courseid', null, PARAM_INT);
 317          require_capability('moodle/grade:manage', context_course::instance($courseid));
 318      }
 319  
 320      /**
 321       * Load in existing data as form defaults
 322       *
 323       * @return void
 324       */
 325      public function set_data_for_dynamic_submission(): void {
 326          $this->set_data((object)[
 327              'courseid' => $this->optional_param('courseid', null, PARAM_INT),
 328              'itemid' => $this->optional_param('itemid', null, PARAM_INT)
 329          ]);
 330      }
 331  
 332      /**
 333       * Returns url to set in $PAGE->set_url() when form is being rendered or submitted via AJAX
 334       *
 335       * @return moodle_url
 336       * @throws \moodle_exception
 337       */
 338      protected function get_page_url_for_dynamic_submission(): moodle_url {
 339          $params = [
 340              'id' => $this->optional_param('courseid', null, PARAM_INT),
 341              'itemid' => $this->optional_param('itemid', null, PARAM_INT),
 342          ];
 343          return new moodle_url('/grade/edit/tree/index.php', $params);
 344      }
 345  
 346      /**
 347       * Process the form submission, used if form was submitted via AJAX
 348       *
 349       * @return array
 350       * @throws \moodle_exception
 351       */
 352      public function process_dynamic_submission() {
 353          global $DB;
 354          $data = $this->get_data();
 355  
 356          $url = $this->gpr->get_return_url('index.php?id=' . $data->courseid);
 357          $local = $this->get_gradeitem();
 358          $gradeitem = $local['gradeitem'];
 359          $item = $local['item'];
 360          $parentcategory = grade_category::fetch_course_category($data->courseid);
 361  
 362          // Form submission handling.
 363          // If unset, give the aggregation values a default based on parent aggregation method.
 364          $defaults = grade_category::get_default_aggregation_coefficient_values($parentcategory->aggregation);
 365          if (!isset($data->aggregationcoef) || $data->aggregationcoef == '') {
 366              $data->aggregationcoef = $defaults['aggregationcoef'];
 367          }
 368          if (!isset($data->weightoverride)) {
 369              $data->weightoverride = $defaults['weightoverride'];
 370          }
 371  
 372          if (property_exists($data, 'calculation')) {
 373              $data->calculation = grade_item::normalize_formula($data->calculation, $data->courseid);
 374          }
 375  
 376          $hide = empty($data->hiddenuntil) ? 0 : $data->hiddenuntil;
 377          if (!$hide) {
 378              $hide = empty($data->hidden) ? 0 : $data->hidden;
 379          }
 380  
 381          $locked   = empty($data->locked) ? 0 : $data->locked;
 382          $locktime = empty($data->locktime) ? 0 : $data->locktime;
 383  
 384          $convert = ['gradepass', 'aggregationcoef', 'aggregationcoef2'];
 385          foreach ($convert as $param) {
 386              if (property_exists($data, $param)) {
 387                  $data->$param = unformat_float($data->$param);
 388              }
 389          }
 390          if (isset($data->aggregationcoef2) && $parentcategory->aggregation == GRADE_AGGREGATE_SUM) {
 391              $data->aggregationcoef2 = $data->aggregationcoef2 / 100.0;
 392          } else {
 393              $data->aggregationcoef2 = $defaults['aggregationcoef2'];
 394          }
 395  
 396          grade_item::set_properties($gradeitem, $data);
 397  
 398          // Link this outcome item to the user specified linked activity.
 399          if (empty($data->cmid) || $data->cmid == 0) {
 400              // Manual item.
 401              $gradeitem->itemtype     = 'manual';
 402              $gradeitem->itemmodule   = null;
 403              $gradeitem->iteminstance = null;
 404              $gradeitem->itemnumber   = 0;
 405  
 406          } else {
 407              $params = [$data->cmid];
 408              $module = $DB->get_record_sql("SELECT cm.*, m.name as modname
 409                                      FROM {modules} m, {course_modules} cm
 410                                     WHERE cm.id = ? AND cm.module = m.id ", $params);
 411              $gradeitem->itemtype     = 'mod';
 412              $gradeitem->itemmodule   = $module->modname;
 413              $gradeitem->iteminstance = $module->instance;
 414  
 415              if ($items = grade_item::fetch_all(['itemtype' => 'mod', 'itemmodule' => $gradeitem->itemmodule,
 416                  'iteminstance' => $gradeitem->iteminstance, 'courseid' => $data->courseid])) {
 417                  if (!empty($gradeitem->id) && in_array($gradeitem, $items)) {
 418                      // No change needed.
 419                  } else {
 420                      $max = 999;
 421                      foreach ($items as $item) {
 422                          if (empty($item->outcomeid)) {
 423                              continue;
 424                          }
 425                          if ($item->itemnumber > $max) {
 426                              $max = $item->itemnumber;
 427                          }
 428                      }
 429                      $gradeitem->itemnumber = $max + 1;
 430                  }
 431              } else {
 432                  $gradeitem->itemnumber = 1000;
 433              }
 434          }
 435  
 436          // Fix scale used.
 437          $outcome = grade_outcome::fetch(['id' => $data->outcomeid]);
 438          $gradeitem->gradetype = GRADE_TYPE_SCALE;
 439          $gradeitem->scaleid = $outcome->scaleid; // TODO: we might recalculate existing outcome grades when changing scale.
 440  
 441          if (empty($gradeitem->id)) {
 442              $gradeitem->insert();
 443              // Move next to activity if adding linked outcome.
 444              if ($gradeitem->itemtype == 'mod') {
 445                  if ($linkeditem = grade_item::fetch(['itemtype' => 'mod', 'itemmodule' => $gradeitem->itemmodule,
 446                      'iteminstance' => $gradeitem->iteminstance, 'itemnumber' => 0, 'courseid' => $data->courseid])) {
 447                      $gradeitem->set_parent($linkeditem->categoryid);
 448                      $gradeitem->move_after_sortorder($linkeditem->sortorder);
 449                  }
 450              } else {
 451                  // Set parent if needed.
 452                  if (isset($data->parentcategory)) {
 453                      $gradeitem->set_parent($data->parentcategory, false);
 454                  }
 455              }
 456  
 457          } else {
 458              $gradeitem->update();
 459          }
 460  
 461          if ($item->cancontrolvisibility) {
 462              // Update hiding flag.
 463              $gradeitem->set_hidden($hide, true);
 464          }
 465  
 466          $gradeitem->set_locktime($locktime); // Locktime first - it might be removed when unlocking.
 467          $gradeitem->set_locked($locked, false, true);
 468          return [
 469              'result' => true,
 470              'url' => $url,
 471              'errors' => [],
 472          ];
 473      }
 474  
 475      /**
 476       * Form validation.
 477       *
 478       * @param array $data array of ("fieldname"=>value) of submitted data
 479       * @param array $files array of uploaded files "element_name"=>tmp_file_path
 480       * @return array of "element_name"=>"error_description" if there are errors,
 481       *         or an empty array if everything is OK (true allowed for backwards compatibility too).
 482       */
 483      public function validation($data, $files): array {
 484          $errors = [];
 485          $local = $this->get_gradeitem();
 486          $gradeitem = $local['gradeitem'];
 487          $item = $local['item'];
 488  
 489          if (!grade_verify_idnumber($gradeitem->id, $item->courseid, $gradeitem)) {
 490              $errors['idnumber'] = get_string('idnumbertaken');
 491          }
 492          return $errors;
 493      }
 494  }