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_plugin_return; 27 use grade_scale; 28 use moodle_url; 29 30 require_once($CFG->dirroot.'/grade/lib.php'); 31 32 /** 33 * Prints the add item 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_item 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 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 $item = $gradeitem->get_record_data(); 58 $parentcategory = $gradeitem->get_parent_category(); 59 } else { 60 $gradeitem = new grade_item(['courseid' => $courseid, 'itemtype' => 'manual'], false); 61 $item = $gradeitem->get_record_data(); 62 $parentcategory = grade_category::fetch_course_category($courseid); 63 } 64 $item->parentcategory = $parentcategory->id; 65 $decimalpoints = $gradeitem->get_decimals(); 66 67 if ($item->hidden > 1) { 68 $item->hiddenuntil = $item->hidden; 69 $item->hidden = 0; 70 } else { 71 $item->hiddenuntil = 0; 72 } 73 74 $item->locked = !empty($item->locked); 75 76 $item->grademax = format_float($item->grademax, $decimalpoints); 77 $item->grademin = format_float($item->grademin, $decimalpoints); 78 79 if ($parentcategory->aggregation == GRADE_AGGREGATE_SUM || $parentcategory->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN2) { 80 $item->aggregationcoef = $item->aggregationcoef == 0 ? 0 : 1; 81 } else { 82 $item->aggregationcoef = format_float($item->aggregationcoef, 4); 83 } 84 if ($parentcategory->aggregation == GRADE_AGGREGATE_SUM) { 85 $item->aggregationcoef2 = format_float($item->aggregationcoef2 * 100.0); 86 } 87 $item->cancontrolvisibility = $gradeitem->can_control_visibility(); 88 return [ 89 'gradeitem' => $gradeitem, 90 'item' => $item 91 ]; 92 } 93 94 /** 95 * Form definition 96 * 97 * @return void 98 * @throws \coding_exception 99 * @throws \dml_exception 100 * @throws \moodle_exception 101 */ 102 protected function definition() { 103 global $CFG; 104 $courseid = $this->optional_param('courseid', null, PARAM_INT); 105 $id = $this->optional_param('itemid', 0, PARAM_INT); 106 $gprplugin = $this->optional_param('gpr_plugin', '', PARAM_TEXT); 107 108 if ($gprplugin && ($gprplugin !== 'tree')) { 109 $this->gpr = new grade_plugin_return(['type' => 'report', 'plugin' => $gprplugin, 'courseid' => $courseid]); 110 } else { 111 $this->gpr = new grade_plugin_return(['type' => 'edit', 'plugin' => 'tree', 'courseid' => $courseid]); 112 } 113 114 $mform =& $this->_form; 115 116 $local = $this->get_gradeitem(); 117 $gradeitem = $local['gradeitem']; 118 $item = $local['item']; 119 120 // Hidden elements. 121 $mform->addElement('hidden', 'id', 0); 122 $mform->setType('id', PARAM_INT); 123 $mform->addElement('hidden', 'courseid', $courseid); 124 $mform->setType('courseid', PARAM_INT); 125 $mform->addElement('hidden', 'itemid', $id); 126 $mform->setType('itemid', PARAM_INT); 127 $mform->addElement('hidden', 'itemtype', 'manual'); // All new items are manual only. 128 $mform->setType('itemtype', PARAM_ALPHA); 129 130 // Visible elements. 131 $mform->addElement('text', 'itemname', get_string('itemname', 'grades')); 132 $mform->setType('itemname', PARAM_TEXT); 133 134 if (!empty($item->id)) { 135 // If grades exist set a message so the user knows why they can not alter the grade type or scale. 136 // We could never change the grade type for external items, so only need to show this for manual grade items. 137 if ($gradeitem->has_grades() && !$gradeitem->is_external_item()) { 138 // Set a message so the user knows why they can not alter the grade type or scale. 139 if ($gradeitem->gradetype == GRADE_TYPE_SCALE) { 140 $gradesexistmsg = get_string('modgradecantchangegradetyporscalemsg', 'grades'); 141 } else { 142 $gradesexistmsg = get_string('modgradecantchangegradetypemsg', 'grades'); 143 } 144 145 $gradesexisthtml = '<div class=\'alert\'>' . $gradesexistmsg . '</div>'; 146 $mform->addElement('static', 'gradesexistmsg', '', $gradesexisthtml); 147 } 148 } 149 150 // Manual grade items cannot have grade type GRADE_TYPE_NONE. 151 $mform->addElement('select', 'gradetype', get_string('gradetype', 'grades'), [ 152 GRADE_TYPE_VALUE => get_string('typevalue', 'grades'), 153 GRADE_TYPE_SCALE => get_string('typescale', 'grades'), 154 GRADE_TYPE_TEXT => get_string('typetext', 'grades') 155 ]); 156 $mform->addHelpButton('gradetype', 'gradetype', 'grades'); 157 $mform->setDefault('gradetype', GRADE_TYPE_VALUE); 158 159 $options = [0 => get_string('usenoscale', 'grades')]; 160 if ($scales = grade_scale::fetch_all_local($courseid)) { 161 foreach ($scales as $scale) { 162 $options[$scale->id] = $scale->get_name(); 163 } 164 } 165 if ($scales = grade_scale::fetch_all_global()) { 166 foreach ($scales as $scale) { 167 $options[$scale->id] = $scale->get_name(); 168 } 169 } 170 $mform->addElement('select', 'scaleid', get_string('scale'), $options); 171 $mform->addHelpButton('scaleid', 'typescale', 'grades'); 172 $mform->hideIf('scaleid', 'gradetype', 'noteq', GRADE_TYPE_SCALE); 173 174 $mform->addElement('select', 'rescalegrades', get_string('modgraderescalegrades', 'grades'), [ 175 '' => get_string('choose'), 176 'no' => get_string('no'), 177 'yes' => get_string('yes') 178 ]); 179 $mform->addHelpButton('rescalegrades', 'modgraderescalegrades', 'grades'); 180 $mform->hideIf('rescalegrades', 'gradetype', 'noteq', GRADE_TYPE_VALUE); 181 182 $mform->addElement('float', 'grademax', get_string('grademax', 'grades')); 183 $mform->addHelpButton('grademax', 'grademax', 'grades'); 184 $mform->hideIf('grademax', 'gradetype', 'noteq', GRADE_TYPE_VALUE); 185 186 if (get_config('moodle', 'grade_report_showmin')) { 187 $mform->addElement('float', 'grademin', get_string('grademin', 'grades')); 188 $mform->addHelpButton('grademin', 'grademin', 'grades'); 189 $mform->hideIf('grademin', 'gradetype', 'noteq', GRADE_TYPE_VALUE); 190 } 191 192 // Hiding. 193 if ($item->cancontrolvisibility) { 194 $mform->addElement('advcheckbox', 'hidden', get_string('hidden', 'grades'), '', [], [0, 1]); 195 $mform->hideIf('hidden', 'hiddenuntil[enabled]', 'checked'); 196 } else { 197 $mform->addElement('static', 'hidden', get_string('hidden', 'grades'), 198 get_string('componentcontrolsvisibility', 'grades')); 199 // Unset hidden to avoid data override. 200 unset($item->hidden); 201 } 202 $mform->addHelpButton('hidden', 'hidden', 'grades'); 203 204 // Locking. 205 $mform->addElement('advcheckbox', 'locked', get_string('locked', 'grades')); 206 $mform->addHelpButton('locked', 'locked', 'grades'); 207 208 // Weight overrides. 209 $mform->addElement('advcheckbox', 'weightoverride', get_string('adjustedweight', 'grades')); 210 $mform->addHelpButton('weightoverride', 'weightoverride', 'grades'); 211 $mform->hideIf('weightoverride', 'gradetype', 'eq', GRADE_TYPE_NONE); 212 $mform->hideIf('weightoverride', 'gradetype', 'eq', GRADE_TYPE_TEXT); 213 214 // Parent category related settings. 215 $mform->addElement('float', 'aggregationcoef2', get_string('weight', 'grades')); 216 $mform->addHelpButton('aggregationcoef2', 'weight', 'grades'); 217 $mform->hideIf('aggregationcoef2', 'weightoverride'); 218 $mform->hideIf('aggregationcoef2', 'gradetype', 'eq', GRADE_TYPE_NONE); 219 $mform->hideIf('aggregationcoef2', 'gradetype', 'eq', GRADE_TYPE_TEXT); 220 221 $options = []; 222 $categories = grade_category::fetch_all(['courseid' => $courseid]); 223 224 foreach ($categories as $cat) { 225 $cat->apply_forced_settings(); 226 $options[$cat->id] = $cat->get_name(); 227 } 228 229 if (count($categories) > 1) { 230 $mform->addElement('select', 'parentcategory', get_string('gradecategory', 'grades'), $options); 231 } 232 233 $parentcategory = $gradeitem->get_parent_category(); 234 if (!$parentcategory) { 235 // If we do not have an id, we are creating a new grade item. 236 237 // Assign the course category to this grade item. 238 $parentcategory = grade_category::fetch_course_category($courseid); 239 $gradeitem->parent_category = $parentcategory; 240 } 241 242 if ($gradeitem->is_external_item()) { 243 // Following items are set up from modules and should not be overrided by user. 244 if ($mform->elementExists('grademin')) { 245 // The site setting grade_report_showmin may have prevented grademin being added to the form. 246 $mform->hardFreeze('grademin'); 247 } 248 $mform->hardFreeze('itemname,gradetype,grademax,scaleid'); 249 250 // For external items we can not change the grade type, even if no grades exist, so if it is set to 251 // scale, then remove the grademax and grademin fields from the form - no point displaying them. 252 if ($gradeitem->gradetype == GRADE_TYPE_SCALE) { 253 $mform->removeElement('grademax'); 254 if ($mform->elementExists('grademin')) { 255 $mform->removeElement('grademin'); 256 } 257 } else { // Not using scale, so remove it. 258 $mform->removeElement('scaleid'); 259 } 260 261 // Always remove the rescale grades element if it's an external item. 262 $mform->removeElement('rescalegrades'); 263 } else if ($gradeitem->has_grades()) { 264 // Can't change the grade type or the scale if there are grades. 265 $mform->hardFreeze('gradetype, scaleid'); 266 267 // If we are using scales then remove the unnecessary rescale and grade fields. 268 if ($gradeitem->gradetype == GRADE_TYPE_SCALE) { 269 $mform->removeElement('rescalegrades'); 270 $mform->removeElement('grademax'); 271 if ($mform->elementExists('grademin')) { 272 $mform->removeElement('grademin'); 273 } 274 } else { // Remove the scale field. 275 $mform->removeElement('scaleid'); 276 // Set the maximum grade to disabled unless a grade is chosen. 277 $mform->hideIf('grademax', 'rescalegrades', 'eq', ''); 278 } 279 } else { 280 // Remove rescale element if there are no grades. 281 $mform->removeElement('rescalegrades'); 282 } 283 284 // If we wanted to change parent of existing item - we would have to verify there are no circular references in parents!!! 285 if ($id > -1 && $mform->elementExists('parentcategory')) { 286 $mform->hardFreeze('parentcategory'); 287 } 288 289 $parentcategory->apply_forced_settings(); 290 291 if (!$parentcategory->is_aggregationcoef_used()) { 292 if ($mform->elementExists('aggregationcoef')) { 293 $mform->removeElement('aggregationcoef'); 294 } 295 296 } else { 297 $coefstring = $gradeitem->get_coefstring(); 298 299 if ($coefstring !== '') { 300 if ($coefstring == 'aggregationcoefextrasum' || $coefstring == 'aggregationcoefextraweightsum') { 301 // The advcheckbox is not compatible with disabledIf! 302 $coefstring = 'aggregationcoefextrasum'; 303 $element =& $mform->createElement('checkbox', 'aggregationcoef', get_string($coefstring, 'grades')); 304 } else { 305 $element =& $mform->createElement('text', 'aggregationcoef', get_string($coefstring, 'grades')); 306 $mform->setType('aggregationcoef', PARAM_FLOAT); 307 } 308 if ($mform->elementExists('parentcategory')) { 309 $mform->insertElementBefore($element, 'parentcategory'); 310 } else { 311 $mform->insertElementBefore($element, 'aggregationcoef2'); 312 } 313 $mform->addHelpButton('aggregationcoef', $coefstring, 'grades'); 314 } 315 $mform->hideIf('aggregationcoef', 'gradetype', 'eq', GRADE_TYPE_NONE); 316 $mform->hideIf('aggregationcoef', 'gradetype', 'eq', GRADE_TYPE_TEXT); 317 $mform->hideIf('aggregationcoef', 'parentcategory', 'eq', $parentcategory->id); 318 } 319 320 // Remove fields used by natural weighting if the parent category is not using natural weighting. 321 // Or if the item is a scale and scales are not used in aggregation. 322 if ($parentcategory->aggregation != GRADE_AGGREGATE_SUM 323 || (empty($CFG->grade_includescalesinaggregation) && $gradeitem->gradetype == GRADE_TYPE_SCALE)) { 324 if ($mform->elementExists('weightoverride')) { 325 $mform->removeElement('weightoverride'); 326 } 327 if ($mform->elementExists('aggregationcoef2')) { 328 $mform->removeElement('aggregationcoef2'); 329 } 330 } 331 332 if ($category = $gradeitem->get_item_category()) { 333 if ($category->aggregation == GRADE_AGGREGATE_SUM) { 334 if ($mform->elementExists('gradetype')) { 335 $mform->hardFreeze('gradetype'); 336 } 337 if ($mform->elementExists('grademin')) { 338 $mform->hardFreeze('grademin'); 339 } 340 if ($mform->elementExists('grademax')) { 341 $mform->hardFreeze('grademax'); 342 } 343 if ($mform->elementExists('scaleid')) { 344 $mform->removeElement('scaleid'); 345 } 346 } 347 } 348 349 $url = new moodle_url('/grade/edit/tree/item.php', ['id' => $id, 'courseid' => $courseid]); 350 $url = $this->gpr->add_url_params($url); 351 $url = '<a class="showadvancedform" href="' . $url . '">' . get_string('showmore', 'form') .'</a>'; 352 $mform->addElement('static', 'advancedform', $url); 353 354 // Add return tracking info. 355 $this->gpr->add_mform_elements($mform); 356 357 $this->set_data($item); 358 } 359 360 /** 361 * Return form context 362 * 363 * @return context 364 */ 365 protected function get_context_for_dynamic_submission(): context { 366 $courseid = $this->optional_param('courseid', null, PARAM_INT); 367 return context_course::instance($courseid); 368 } 369 370 /** 371 * Check if current user has access to this form, otherwise throw exception 372 * 373 * @return void 374 * @throws \required_capability_exception 375 */ 376 protected function check_access_for_dynamic_submission(): void { 377 $courseid = $this->optional_param('courseid', null, PARAM_INT); 378 require_capability('moodle/grade:manage', context_course::instance($courseid)); 379 } 380 381 /** 382 * Load in existing data as form defaults 383 * 384 * @return void 385 */ 386 public function set_data_for_dynamic_submission(): void { 387 $this->set_data((object)[ 388 'courseid' => $this->optional_param('courseid', null, PARAM_INT), 389 'itemid' => $this->optional_param('itemid', null, PARAM_INT) 390 ]); 391 } 392 393 /** 394 * Returns url to set in $PAGE->set_url() when form is being rendered or submitted via AJAX 395 * 396 * @return moodle_url 397 * @throws \moodle_exception 398 */ 399 protected function get_page_url_for_dynamic_submission(): moodle_url { 400 $params = [ 401 'id' => $this->optional_param('courseid', null, PARAM_INT), 402 'itemid' => $this->optional_param('itemid', null, PARAM_INT), 403 ]; 404 return new moodle_url('/grade/edit/tree/index.php', $params); 405 } 406 407 /** 408 * Process the form submission, used if form was submitted via AJAX 409 * 410 * @return array 411 * @throws \moodle_exception 412 */ 413 public function process_dynamic_submission() { 414 $data = $this->get_data(); 415 416 $url = $this->gpr->get_return_url('index.php?id=' . $data->courseid); 417 $local = $this->get_gradeitem(); 418 $gradeitem = $local['gradeitem']; 419 $item = $local['item']; 420 $parentcategory = grade_category::fetch_course_category($data->courseid); 421 422 // Form submission handling. 423 424 // This is a new item, and the category chosen is different than the default category. 425 if (empty($gradeitem->id) && isset($data->parentcategory) && $parentcategory->id != $data->parentcategory) { 426 $parentcategory = grade_category::fetch(['id' => $data->parentcategory]); 427 } 428 429 // If unset, give the aggregation values a default based on parent aggregation method. 430 $defaults = grade_category::get_default_aggregation_coefficient_values($parentcategory->aggregation); 431 if (!isset($data->aggregationcoef) || $data->aggregationcoef == '') { 432 $data->aggregationcoef = $defaults['aggregationcoef']; 433 } 434 if (!isset($data->weightoverride)) { 435 $data->weightoverride = $defaults['weightoverride']; 436 } 437 438 if (!isset($data->gradepass) || $data->gradepass == '') { 439 $data->gradepass = 0; 440 } 441 442 if (!isset($data->grademin) || $data->grademin == '') { 443 $data->grademin = 0; 444 } 445 446 $hide = empty($data->hiddenuntil) ? 0 : $data->hiddenuntil; 447 if (!$hide) { 448 $hide = empty($data->hidden) ? 0 : $data->hidden; 449 } 450 451 $locked = empty($data->locked) ? 0 : $data->locked; 452 $locktime = empty($data->locktime) ? 0 : $data->locktime; 453 454 $convert = ['grademax', 'grademin', 'aggregationcoef', 'aggregationcoef2']; 455 foreach ($convert as $param) { 456 if (property_exists($data, $param)) { 457 $data->$param = unformat_float($data->$param); 458 } 459 } 460 if (isset($data->aggregationcoef2) && $parentcategory->aggregation == GRADE_AGGREGATE_SUM) { 461 $data->aggregationcoef2 = $data->aggregationcoef2 / 100.0; 462 } else { 463 $data->aggregationcoef2 = $defaults['aggregationcoef2']; 464 } 465 466 $oldmin = $gradeitem->grademin; 467 $oldmax = $gradeitem->grademax; 468 grade_item::set_properties($gradeitem, $data); 469 $gradeitem->outcomeid = null; 470 471 // Handle null decimals value. 472 if (!property_exists($data, 'decimals') || $data->decimals < 0) { 473 $gradeitem->decimals = null; 474 } 475 476 if (empty($gradeitem->id)) { 477 $gradeitem->itemtype = 'manual'; // All new items to be manual only. 478 $gradeitem->insert(); 479 480 // Set parent if needed. 481 if (isset($data->parentcategory)) { 482 $gradeitem->set_parent($data->parentcategory, false); 483 } 484 485 } else { 486 $gradeitem->update(); 487 488 if (!empty($data->rescalegrades) && $data->rescalegrades == 'yes') { 489 $newmin = $gradeitem->grademin; 490 $newmax = $gradeitem->grademax; 491 $gradeitem->rescale_grades_keep_percentage($oldmin, $oldmax, $newmin, $newmax, 'gradebook'); 492 } 493 } 494 495 if ($item->cancontrolvisibility) { 496 // Update hiding flag. 497 $gradeitem->set_hidden($hide, true); 498 } 499 500 $gradeitem->set_locktime($locktime); // Locktime first - it might be removed when unlocking. 501 $gradeitem->set_locked($locked); 502 return [ 503 'result' => true, 504 'url' => $url, 505 'errors' => [], 506 ]; 507 } 508 509 /** 510 * Form validation. 511 * 512 * @param array $data array of ("fieldname"=>value) of submitted data 513 * @param array $files array of uploaded files "element_name"=>tmp_file_path 514 * @return array of "element_name"=>"error_description" if there are errors, 515 * or an empty array if everything is OK (true allowed for backwards compatibility too). 516 */ 517 public function validation($data, $files): array { 518 $errors = []; 519 $local = $this->get_gradeitem(); 520 $gradeitem = $local['gradeitem']; 521 522 if (isset($data['gradetype']) && $data['gradetype'] == GRADE_TYPE_SCALE) { 523 if (empty($data['scaleid'])) { 524 $errors['scaleid'] = get_string('missingscale', 'grades'); 525 } 526 } 527 528 // We need to make all the validations related with grademax and grademin 529 // with them being correct floats, keeping the originals unmodified for 530 // later validations / showing the form back... 531 // TODO: Note that once MDL-73994 is fixed we'll have to re-visit this and 532 // adapt the code below to the new values arriving here, without forgetting 533 // the special case of empties and nulls. 534 $grademax = isset($data['grademax']) ? unformat_float($data['grademax']) : null; 535 $grademin = isset($data['grademin']) ? unformat_float($data['grademin']) : null; 536 537 if (!is_null($grademin) && !is_null($grademax)) { 538 if ($grademax == $grademin || $grademax < $grademin) { 539 $errors['grademin'] = get_string('incorrectminmax', 'grades'); 540 $errors['grademax'] = get_string('incorrectminmax', 'grades'); 541 } 542 } 543 544 // We do not want the user to be able to change the grade type or scale for this item if grades exist. 545 if ($gradeitem && $gradeitem->has_grades()) { 546 // Check that grade type is set - should never not be set unless form has been modified. 547 if (!isset($data['gradetype'])) { 548 $errors['gradetype'] = get_string('modgradecantchangegradetype', 'grades'); 549 } else if ($data['gradetype'] !== $gradeitem->gradetype) { // Check if we are changing the grade type. 550 $errors['gradetype'] = get_string('modgradecantchangegradetype', 'grades'); 551 } else if ($data['gradetype'] == GRADE_TYPE_SCALE) { 552 // Check if we are changing the scale - can't do this when grades exist. 553 if (isset($data['scaleid']) && ($data['scaleid'] !== $gradeitem->scaleid)) { 554 $errors['scaleid'] = get_string('modgradecantchangescale', 'grades'); 555 } 556 } 557 } 558 if ($gradeitem) { 559 if ($gradeitem->gradetype == GRADE_TYPE_VALUE) { 560 if ((((bool) get_config('moodle', 'grade_report_showmin')) && 561 grade_floats_different($grademin, $gradeitem->grademin)) || 562 grade_floats_different($grademax, $gradeitem->grademax)) { 563 if ($gradeitem->has_grades() && empty($data['rescalegrades'])) { 564 $errors['rescalegrades'] = get_string('mustchooserescaleyesorno', 'grades'); 565 } 566 } 567 } 568 } 569 return $errors; 570 } 571 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body