Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402] [Versions 401 and 402] [Versions 402 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 * A library of classes used by the grade edit pages 19 * 20 * @package core_grades 21 * @copyright 2009 Nicolas Connault 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 25 class grade_edit_tree { 26 public $columns = array(); 27 28 /** 29 * @var grade_tree $gtree @see grade/lib.php 30 */ 31 public $gtree; 32 33 /** 34 * @var grade_plugin_return @see grade/lib.php 35 */ 36 public $gpr; 37 38 /** 39 * @var string $moving The eid of the category or item being moved 40 */ 41 public $moving; 42 43 public $deepest_level; 44 45 public $uses_weight = false; 46 47 public $table; 48 49 public $categories = array(); 50 51 /** 52 * Constructor 53 */ 54 public function __construct($gtree, $moving, $gpr) { 55 global $USER, $OUTPUT, $COURSE; 56 57 $this->gtree = $gtree; 58 $this->moving = $moving; 59 $this->gpr = $gpr; 60 $this->deepest_level = $this->get_deepest_level($this->gtree->top_element); 61 62 $this->columns = array(grade_edit_tree_column::factory('name', array('deepest_level' => $this->deepest_level))); 63 64 if ($this->uses_weight) { 65 $this->columns[] = grade_edit_tree_column::factory('weight', array('adv' => 'weight')); 66 } 67 68 $this->columns[] = grade_edit_tree_column::factory('range'); // This is not a setting... How do we deal with it? 69 $this->columns[] = grade_edit_tree_column::factory('status'); 70 $this->columns[] = grade_edit_tree_column::factory('actions'); 71 72 if ($this->deepest_level > 1) { 73 $this->columns[] = grade_edit_tree_column::factory('select'); 74 } 75 76 $this->table = new html_table(); 77 $this->table->id = "grade_edit_tree_table"; 78 $this->table->attributes['class'] = 'generaltable simple setup-grades'; 79 if ($this->moving) { 80 $this->table->attributes['class'] .= ' moving'; 81 } 82 83 foreach ($this->columns as $column) { 84 if (!($this->moving && $column->hide_when_moving)) { 85 $this->table->head[] = $column->get_header_cell(); 86 } 87 } 88 89 $rowcount = 0; 90 $this->table->data = $this->build_html_tree($this->gtree->top_element, true, array(), 0, $rowcount); 91 } 92 93 /** 94 * Recursive function for building the table holding the grade categories and items, 95 * with CSS indentation and styles. 96 * 97 * @param array $element The current tree element being rendered 98 * @param boolean $totals Whether or not to print category grade items (category totals) 99 * @param array $parents An array of parent categories for the current element (used for indentation and row classes) 100 * 101 * @return string HTML 102 */ 103 public function build_html_tree($element, $totals, $parents, $level, &$row_count) { 104 global $CFG, $COURSE, $PAGE, $OUTPUT; 105 106 $object = $element['object']; 107 $eid = $element['eid']; 108 $name = $this->gtree->get_element_header($element, true, false, true, false, true); 109 $icon = $this->gtree->get_element_icon($element); 110 $type = $this->gtree->get_element_type_string($element); 111 $strippedname = $this->gtree->get_element_header($element, false, false, false); 112 $is_category_item = false; 113 if ($element['type'] == 'categoryitem' || $element['type'] == 'courseitem') { 114 $is_category_item = true; 115 } 116 117 $rowclasses = array(); 118 foreach ($parents as $parent_eid) { 119 $rowclasses[] = $parent_eid; 120 } 121 122 $moveaction = ''; 123 $actions = $this->gtree->get_cell_action_menu($element, 'setup', $this->gpr); 124 125 if ($element['type'] == 'item' or ($element['type'] == 'category' and $element['depth'] > 1)) { 126 $aurl = new moodle_url('index.php', array('id' => $COURSE->id, 'action' => 'moveselect', 'eid' => $eid, 'sesskey' => sesskey())); 127 $moveaction .= $OUTPUT->action_icon($aurl, new pix_icon('t/move', get_string('move'))); 128 } 129 130 $returnrows = array(); 131 $root = false; 132 133 $id = required_param('id', PARAM_INT); 134 135 /// prepare move target if needed 136 $last = ''; 137 138 /// print the list items now 139 if ($this->moving == $eid) { 140 // do not diplay children 141 $cell = new html_table_cell(); 142 $cell->colspan = 12; 143 $cell->attributes['class'] = $element['type'] . ' moving column-name level' . 144 ($level + 1) . ' level' . ($level % 2 ? 'even' : 'odd'); 145 $cell->text = $name.' ('.get_string('move').')'; 146 147 // Create a row that represents the available area to move a grade item or a category into. 148 $movingarea = new html_table_row(); 149 // Obtain all parent category identifiers for this item and add them to its class list. This information 150 // will be used when collapsing or expanding grade categories to properly show or hide this area. 151 $parentcategories = array_merge($rowclasses, [$eid]); 152 $movingarea->attributes = [ 153 'class' => implode(' ', $parentcategories), 154 'data-hidden' => 'false' 155 ]; 156 $movingarea->cells[] = $cell; 157 158 return [$movingarea]; 159 } 160 161 if ($element['type'] == 'category') { 162 $level++; 163 $this->categories[$object->id] = $strippedname; 164 $category = grade_category::fetch(array('id' => $object->id)); 165 $item = $category->get_grade_item(); 166 167 // Add aggregation coef input if not a course item and if parent category has correct aggregation type 168 // Before we print the category's row, we must find out how many rows will appear below it (for the filler cell's rowspan) 169 $aggregation_position = grade_get_setting($COURSE->id, 'aggregationposition', $CFG->grade_aggregationposition); 170 $category_total_data = null; // Used if aggregationposition is set to "last", so we can print it last 171 172 $html_children = array(); 173 174 // Take into consideration that a category item always has an empty row (spacer) below. 175 $row_count = 1; 176 177 foreach($element['children'] as $child_el) { 178 $moveto = null; 179 180 if (empty($child_el['object']->itemtype)) { 181 $child_el['object']->itemtype = false; 182 } 183 184 if (($child_el['object']->itemtype == 'course' || $child_el['object']->itemtype == 'category') && !$totals) { 185 continue; 186 } 187 188 $child_eid = $child_el['eid']; 189 $first = ''; 190 191 if ($child_el['object']->itemtype == 'course' || $child_el['object']->itemtype == 'category') { 192 $first = array('first' => 1); 193 $child_eid = $eid; 194 } 195 196 if ($this->moving && $this->moving != $child_eid) { 197 198 $strmove = get_string('move'); 199 $actions = $moveaction = ''; // no action icons when moving 200 201 $aurl = new moodle_url('index.php', array('id' => $COURSE->id, 'action' => 'move', 'eid' => $this->moving, 'moveafter' => $child_eid, 'sesskey' => sesskey())); 202 if ($first) { 203 $aurl->params($first); 204 } 205 206 $cell = new html_table_cell(); 207 $cell->colspan = 12; 208 $cell->attributes['class'] = 'movehere level' . ($level + 1) . ' level' . ($level % 2 ? 'even' : 'odd'); 209 210 $cell->text = html_writer::link($aurl, html_writer::empty_tag('hr'), 211 ['title' => get_string('movehere'), 'class' => 'movehere']); 212 213 // Create a row that represents the available area to move a grade item or a category into. 214 $moveto = new html_table_row(); 215 // Obtain all parent category identifiers for this item and add them to its class list. This information 216 // will be used when collapsing or expanding grade categories to properly show or hide this area. 217 $parentcategories = array_merge($rowclasses, [$eid]); 218 $moveto->attributes['class'] = implode(' ', $parentcategories); 219 $moveto->attributes['data-hidden'] = 'false'; 220 $moveto->cells[] = $cell; 221 } 222 223 $newparents = $parents; 224 $newparents[] = $eid; 225 226 $row_count++; 227 $child_row_count = 0; 228 229 // If moving, do not print course and category totals, but still print the moveto target box 230 if ($this->moving && ($child_el['object']->itemtype == 'course' || $child_el['object']->itemtype == 'category')) { 231 $html_children[] = $moveto; 232 } elseif ($child_el['object']->itemtype == 'course' || $child_el['object']->itemtype == 'category') { 233 // We don't build the item yet because we first need to know the deepest level of categories (for category/name colspans) 234 $category_total_item = $this->build_html_tree($child_el, $totals, $newparents, $level, $child_row_count); 235 if (!$aggregation_position) { 236 $html_children = array_merge($html_children, $category_total_item); 237 } 238 } else { 239 $html_children = array_merge($html_children, $this->build_html_tree($child_el, $totals, $newparents, $level, $child_row_count)); 240 if (!empty($moveto)) { 241 $html_children[] = $moveto; 242 } 243 244 if ($this->moving) { 245 $row_count++; 246 } 247 } 248 249 $row_count += $child_row_count; 250 251 // If the child is a category, increment row_count by one more (for the extra coloured row) 252 if ($child_el['type'] == 'category') { 253 $row_count++; 254 } 255 } 256 257 // Print category total at the end if aggregation position is "last" (1) 258 if (!empty($category_total_item) && $aggregation_position) { 259 $html_children = array_merge($html_children, $category_total_item); 260 } 261 262 // Determine if we are at the root 263 if (isset($element['object']->grade_item) && $element['object']->grade_item->is_course_item()) { 264 $root = true; 265 } 266 267 $levelclass = "level$level level" . ($level % 2 ? 'odd' : 'even'); 268 269 $courseclass = ''; 270 if ($level == 1) { 271 $courseclass = 'coursecategory'; 272 } 273 274 $categoryrow = new html_table_row(); 275 $categoryrow->id = 'grade-item-' . $eid; 276 $categoryrow->attributes['class'] = $courseclass . ' category '; 277 $categoryrow->attributes['data-category'] = $eid; 278 $categoryrow->attributes['data-itemid'] = $category->get_grade_item()->id; 279 $categoryrow->attributes['data-hidden'] = 'false'; 280 foreach ($rowclasses as $class) { 281 $categoryrow->attributes['class'] .= ' ' . $class; 282 } 283 284 foreach ($this->columns as $column) { 285 if (!($this->moving && $column->hide_when_moving)) { 286 $categoryrow->cells[] = $column->get_category_cell($category, $levelclass, [ 287 'id' => $id, 288 'name' => $name, 289 'level' => $level, 290 'actions' => $actions, 291 'moveaction' => $moveaction, 292 'eid' => $eid, 293 ]); 294 } 295 } 296 297 $emptyrow = new html_table_row(); 298 // Obtain all parent category identifiers for this item and add them to its class list. This information 299 // will be used when collapsing or expanding grade categories to properly show or hide this area. 300 $parentcategories = array_merge($rowclasses, [$eid]); 301 $emptyrow->attributes['class'] = 'spacer ' . implode(' ', $parentcategories); 302 $emptyrow->attributes['data-hidden'] = 'false'; 303 $emptyrow->attributes['aria-hidden'] = 'true'; 304 305 $headercell = new html_table_cell(); 306 $headercell->header = true; 307 $headercell->scope = 'row'; 308 $headercell->attributes['class'] = 'cell column-rowspan rowspan'; 309 $headercell->attributes['aria-hidden'] = 'true'; 310 $headercell->rowspan = $row_count; 311 $emptyrow->cells[] = $headercell; 312 313 $returnrows = array_merge([$categoryrow, $emptyrow], $html_children); 314 315 // Print a coloured row to show the end of the category across the table 316 $endcell = new html_table_cell(); 317 $endcell->colspan = (19 - $level); 318 $endcell->attributes['class'] = 'emptyrow colspan ' . $levelclass; 319 $endcell->attributes['aria-hidden'] = 'true'; 320 321 $returnrows[] = new html_table_row(array($endcell)); 322 323 } else { // Dealing with a grade item 324 325 $item = grade_item::fetch(array('id' => $object->id)); 326 $element['type'] = 'item'; 327 $element['object'] = $item; 328 329 $categoryitemclass = ''; 330 if ($item->itemtype == 'category') { 331 $categoryitemclass = 'categoryitem'; 332 } 333 if ($item->itemtype == 'course') { 334 $categoryitemclass = 'courseitem'; 335 } 336 337 $gradeitemrow = new html_table_row(); 338 $gradeitemrow->id = 'grade-item-' . $eid; 339 $gradeitemrow->attributes['class'] = $categoryitemclass . ' item '; 340 $gradeitemrow->attributes['data-itemid'] = $object->id; 341 $gradeitemrow->attributes['data-hidden'] = 'false'; 342 // If this item is a course or category aggregation, add a data attribute that stores the identifier of 343 // the related category or course. This attribute is used when collapsing a grade category to fetch the 344 // max grade from the aggregation and display it in the grade category row when the category items are 345 // collapsed and the aggregated max grade is not visible. 346 if (!empty($categoryitemclass)) { 347 $gradeitemrow->attributes['data-aggregationforcategory'] = $parent_eid; 348 } 349 foreach ($rowclasses as $class) { 350 $gradeitemrow->attributes['class'] .= ' ' . $class; 351 } 352 353 foreach ($this->columns as $column) { 354 if (!($this->moving && $column->hide_when_moving)) { 355 $gradeitemrow->cells[] = $column->get_item_cell( 356 $item, 357 [ 358 'id' => $id, 359 'name' => $name, 360 'level' => $level, 361 'actions' => $actions, 362 'element' => $element, 363 'eid' => $eid, 364 'moveaction' => $moveaction, 365 'itemtype' => $object->itemtype, 366 'icon' => $icon, 367 'type' => $type 368 ] 369 ); 370 } 371 } 372 373 $returnrows[] = $gradeitemrow; 374 } 375 376 return $returnrows; 377 378 } 379 380 /** 381 * Given a grade_item object, returns a labelled input if an aggregation coefficient (weight or extra credit) applies to it. 382 * @param grade_item $item 383 * @return string HTML 384 */ 385 static function get_weight_input($item) { 386 global $OUTPUT; 387 388 if (!is_object($item) || get_class($item) !== 'grade_item') { 389 throw new Exception('grade_edit_tree::get_weight_input($item) was given a variable that is not of the required type (grade_item object)'); 390 return false; 391 } 392 393 if ($item->is_course_item()) { 394 return ''; 395 } 396 397 $parent_category = $item->get_parent_category(); 398 $parent_category->apply_forced_settings(); 399 $aggcoef = $item->get_coefstring(); 400 401 $itemname = $item->itemname; 402 if ($item->is_category_item()) { 403 // Remember, the parent category of a category item is the category itself. 404 $itemname = $parent_category->get_name(); 405 } 406 $str = ''; 407 408 if ($aggcoef == 'aggregationcoefweight' || $aggcoef == 'aggregationcoef' || $aggcoef == 'aggregationcoefextraweight') { 409 410 return $OUTPUT->render_from_template('core_grades/weight_field', [ 411 'id' => $item->id, 412 'itemname' => $itemname, 413 'value' => self::format_number($item->aggregationcoef) 414 ]); 415 416 } else if ($aggcoef == 'aggregationcoefextraweightsum') { 417 418 $tpldata = [ 419 'id' => $item->id, 420 'itemname' => $itemname, 421 'value' => self::format_number($item->aggregationcoef2 * 100.0), 422 'checked' => $item->weightoverride, 423 'disabled' => !$item->weightoverride 424 ]; 425 $str .= $OUTPUT->render_from_template('core_grades/weight_override_field', $tpldata); 426 427 } 428 429 return $str; 430 } 431 432 // Trims trailing zeros. 433 // Used on the 'Gradebook setup' page for grade items settings like aggregation co-efficient. 434 // Grader report has its own decimal place settings so they are handled elsewhere. 435 static function format_number($number) { 436 $formatted = rtrim(format_float($number, 4),'0'); 437 if (substr($formatted, -1)==get_string('decsep', 'langconfig')) { //if last char is the decimal point 438 $formatted .= '0'; 439 } 440 return $formatted; 441 } 442 443 /** 444 * Given an element of the grade tree, returns whether it is deletable or not (only manual grade items are deletable) 445 * 446 * @param array $element 447 * @return bool 448 */ 449 public static function element_deletable($element) { 450 global $COURSE; 451 452 if ($element['type'] != 'item') { 453 return true; 454 } 455 456 $grade_item = $element['object']; 457 458 if ($grade_item->itemtype != 'mod' or $grade_item->is_outcome_item() or $grade_item->gradetype == GRADE_TYPE_NONE) { 459 return true; 460 } 461 462 $modinfo = get_fast_modinfo($COURSE); 463 if (!isset($modinfo->instances[$grade_item->itemmodule][$grade_item->iteminstance])) { 464 // module does not exist 465 return true; 466 } 467 468 return false; 469 } 470 471 /** 472 * Given an element of the grade tree, returns whether it is duplicatable or not (only manual grade items are duplicatable) 473 * 474 * @param array $element 475 * @return bool 476 */ 477 public static function element_duplicatable($element) { 478 if ($element['type'] != 'item') { 479 return false; 480 } 481 482 $gradeitem = $element['object']; 483 if ($gradeitem->itemtype != 'mod') { 484 return true; 485 } 486 return false; 487 } 488 489 /** 490 * Given the grade tree and an array of element ids (e.g. c15, i42), and expecting the 'moveafter' URL param, 491 * moves the selected items to the requested location. Then redirects the user to the given $returnurl 492 * 493 * @param object $gtree The grade tree (a recursive representation of the grade categories and grade items) 494 * @param array $eids 495 * @param string $returnurl 496 */ 497 function move_elements($eids, $returnurl) { 498 $moveafter = required_param('moveafter', PARAM_INT); 499 500 if (!is_array($eids)) { 501 $eids = array($eids); 502 } 503 504 if(!$after_el = $this->gtree->locate_element("cg$moveafter")) { 505 throw new \moodle_exception('invalidelementid', '', $returnurl); 506 } 507 508 $after = $after_el['object']; 509 $parent = $after; 510 $sortorder = $after->get_sortorder(); 511 512 foreach ($eids as $eid) { 513 if (!$element = $this->gtree->locate_element($eid)) { 514 throw new \moodle_exception('invalidelementid', '', $returnurl); 515 } 516 $object = $element['object']; 517 518 $object->set_parent($parent->id); 519 $object->move_after_sortorder($sortorder); 520 $sortorder++; 521 } 522 523 redirect($returnurl, '', 0); 524 } 525 526 /** 527 * Recurses through the entire grade tree to find and return the maximum depth of the tree. 528 * This should be run only once from the root element (course category), and is used for the 529 * indentation of the Name column's cells (colspan) 530 * 531 * @param array $element An array of values representing a grade tree's element (all grade items in this case) 532 * @param int $level The level of the current recursion 533 * @param int $deepest_level A value passed to each subsequent level of recursion and incremented if $level > $deepest_level 534 * @return int Deepest level 535 */ 536 function get_deepest_level($element, $level=0, $deepest_level=1) { 537 $object = $element['object']; 538 539 $level++; 540 $coefstring = $element['object']->get_coefstring(); 541 if ($element['type'] == 'category') { 542 if ($coefstring == 'aggregationcoefweight' || $coefstring == 'aggregationcoefextraweightsum' || 543 $coefstring == 'aggregationcoefextraweight') { 544 $this->uses_weight = true; 545 } 546 547 foreach($element['children'] as $child_el) { 548 if ($level > $deepest_level) { 549 $deepest_level = $level; 550 } 551 $deepest_level = $this->get_deepest_level($child_el, $level, $deepest_level); 552 } 553 554 $category = grade_category::fetch(array('id' => $object->id)); 555 $item = $category->get_grade_item(); 556 if ($item->gradetype == GRADE_TYPE_NONE) { 557 // Add 1 more level for grade category that has no total. 558 $deepest_level++; 559 } 560 } 561 562 return $deepest_level; 563 } 564 565 /** 566 * Updates the provided gradecategory item with the provided data. 567 * 568 * @param grade_category $gradecategory The category to update. 569 * @param stdClass $data the data to update the category with. 570 * @return void 571 */ 572 public static function update_gradecategory(grade_category $gradecategory, stdClass $data) { 573 // If no fullname is entered for a course category, put ? in the DB. 574 if (!isset($data->fullname) || $data->fullname == '') { 575 $data->fullname = '?'; 576 } 577 578 if (!isset($data->aggregateonlygraded)) { 579 $data->aggregateonlygraded = 0; 580 } 581 if (!isset($data->aggregateoutcomes)) { 582 $data->aggregateoutcomes = 0; 583 } 584 grade_category::set_properties($gradecategory, $data); 585 586 // CATEGORY. 587 if (empty($gradecategory->id)) { 588 $gradecategory->insert(); 589 590 } else { 591 $gradecategory->update(); 592 } 593 594 // GRADE ITEM. 595 // Grade item data saved with prefix "grade_item_". 596 $itemdata = new stdClass(); 597 foreach ($data as $k => $v) { 598 if (preg_match('/grade_item_(.*)/', $k, $matches)) { 599 $itemdata->{$matches[1]} = $v; 600 } 601 } 602 603 if (!isset($itemdata->aggregationcoef)) { 604 $itemdata->aggregationcoef = 0; 605 } 606 607 if (!isset($itemdata->gradepass) || $itemdata->gradepass == '') { 608 $itemdata->gradepass = 0; 609 } 610 611 if (!isset($itemdata->grademax) || $itemdata->grademax == '') { 612 $itemdata->grademax = 0; 613 } 614 615 if (!isset($itemdata->grademin) || $itemdata->grademin == '') { 616 $itemdata->grademin = 0; 617 } 618 619 $hidden = empty($itemdata->hidden) ? 0 : $itemdata->hidden; 620 $hiddenuntil = empty($itemdata->hiddenuntil) ? 0 : $itemdata->hiddenuntil; 621 unset($itemdata->hidden); 622 unset($itemdata->hiddenuntil); 623 624 $locked = empty($itemdata->locked) ? 0 : $itemdata->locked; 625 $locktime = empty($itemdata->locktime) ? 0 : $itemdata->locktime; 626 unset($itemdata->locked); 627 unset($itemdata->locktime); 628 629 $convert = array('grademax', 'grademin', 'gradepass', 'multfactor', 'plusfactor', 'aggregationcoef', 'aggregationcoef2'); 630 foreach ($convert as $param) { 631 if (property_exists($itemdata, $param)) { 632 $itemdata->$param = unformat_float($itemdata->$param); 633 } 634 } 635 if (isset($itemdata->aggregationcoef2)) { 636 $itemdata->aggregationcoef2 = $itemdata->aggregationcoef2 / 100.0; 637 } 638 639 // When creating a new category, a number of grade item fields are filled out automatically, and are required. 640 // If the user leaves these fields empty during creation of a category, we let the default values take effect. 641 // Otherwise, we let the user-entered grade item values take effect. 642 $gradeitem = $gradecategory->load_grade_item(); 643 $gradeitemcopy = fullclone($gradeitem); 644 grade_item::set_properties($gradeitem, $itemdata); 645 646 if (empty($gradeitem->id)) { 647 $gradeitem->id = $gradeitemcopy->id; 648 } 649 if (empty($gradeitem->grademax) && $gradeitem->grademax != '0') { 650 $gradeitem->grademax = $gradeitemcopy->grademax; 651 } 652 if (empty($gradeitem->grademin) && $gradeitem->grademin != '0') { 653 $gradeitem->grademin = $gradeitemcopy->grademin; 654 } 655 if (empty($gradeitem->gradepass) && $gradeitem->gradepass != '0') { 656 $gradeitem->gradepass = $gradeitemcopy->gradepass; 657 } 658 if (empty($gradeitem->aggregationcoef) && $gradeitem->aggregationcoef != '0') { 659 $gradeitem->aggregationcoef = $gradeitemcopy->aggregationcoef; 660 } 661 662 // Handle null decimals value - must be done before update! 663 if (!property_exists($itemdata, 'decimals') or $itemdata->decimals < 0) { 664 $gradeitem->decimals = null; 665 } 666 667 // Change weightoverride flag. Check if the value is set, because it is not when the checkbox is not ticked. 668 $itemdata->weightoverride = isset($itemdata->weightoverride) ? $itemdata->weightoverride : 0; 669 if ($gradeitem->weightoverride != $itemdata->weightoverride && $gradecategory->aggregation == GRADE_AGGREGATE_SUM) { 670 // If we are using natural weight and the weight has been un-overriden, force parent category to recalculate weights. 671 $gradecategory->force_regrading(); 672 } 673 $gradeitem->weightoverride = $itemdata->weightoverride; 674 675 $gradeitem->outcomeid = null; 676 677 // This means we want to rescale overridden grades as well. 678 if (!empty($data->grade_item_rescalegrades) && $data->grade_item_rescalegrades == 'yes') { 679 $gradeitem->markasoverriddenwhengraded = false; 680 $gradeitem->rescale_grades_keep_percentage($gradeitemcopy->grademin, $gradeitemcopy->grademax, 681 $gradeitem->grademin, $gradeitem->grademax, 'gradebook'); 682 } 683 684 // Only update the category's 'hidden' status if it has changed. Leaving a category as 'unhidden' (checkbox left 685 // unmarked) and submitting the form without this conditional check will result in displaying any grade items that 686 // are in the category, including those that were previously 'hidden'. 687 if (($gradecategory->get_hidden() != $hiddenuntil) || ($gradecategory->get_hidden() != $hidden)) { 688 if ($hiddenuntil) { 689 $gradecategory->set_hidden($hiddenuntil, true); 690 } else { 691 $gradecategory->set_hidden($hidden, true); 692 } 693 } 694 695 $gradeitem->set_locktime($locktime); // Locktime first - it might be removed when unlocking. 696 $gradeitem->set_locked($locked, false, true); 697 698 $gradeitem->update(); // We don't need to insert it, it's already created when the category is created. 699 700 // Set parent if needed. 701 if (isset($data->parentcategory)) { 702 $gradecategory->set_parent($data->parentcategory, 'gradebook'); 703 } 704 } 705 } 706 707 /** 708 * Class grade_edit_tree_column 709 * 710 * @package core_grades 711 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 712 */ 713 abstract class grade_edit_tree_column { 714 public $forced; 715 public $hidden; 716 public $forced_hidden; 717 public $advanced_hidden; 718 public $hide_when_moving = true; 719 /** 720 * html_table_cell object used as a template for header cells in all categories. 721 * It must be cloned before being used. 722 * @var html_table_cell $headercell 723 */ 724 public $headercell; 725 /** 726 * html_table_cell object used as a template for category cells in all categories. 727 * It must be cloned before being used. 728 * @var html_table_cell $categorycell 729 */ 730 public $categorycell; 731 /** 732 * html_table_cell object used as a template for item cells in all categories. 733 * It must be cloned before being used. 734 * @var html_table_cell $itemcell 735 */ 736 public $itemcell; 737 738 public static function factory($name, $params=array()) { 739 $class_name = "grade_edit_tree_column_$name"; 740 if (class_exists($class_name)) { 741 return new $class_name($params); 742 } 743 } 744 745 public abstract function get_header_cell(); 746 747 public function get_category_cell($category, $levelclass, $params) { 748 $cell = clone($this->categorycell); 749 $cell->attributes['class'] .= ' ' . $levelclass; 750 return $cell; 751 } 752 753 public function get_item_cell($item, $params) { 754 $cell = clone($this->itemcell); 755 if (isset($params['level'])) { 756 $level = $params['level'] + (($item->itemtype == 'category' || $item->itemtype == 'course') ? 0 : 1); 757 $cell->attributes['class'] .= ' level' . $level; 758 $cell->attributes['class'] .= ' level' . ($level % 2 ? 'odd' : 'even'); 759 } 760 return $cell; 761 } 762 763 public function __construct() { 764 $this->headercell = new html_table_cell(); 765 $this->headercell->header = true; 766 $this->headercell->attributes['class'] = 'header'; 767 768 $this->categorycell = new html_table_cell(); 769 $this->categorycell->attributes['class'] = 'cell'; 770 771 $this->itemcell = new html_table_cell(); 772 $this->itemcell->attributes['class'] = 'cell'; 773 774 if (preg_match('/^grade_edit_tree_column_(\w*)$/', get_class($this), $matches)) { 775 $this->headercell->attributes['class'] .= ' column-' . $matches[1]; 776 $this->categorycell->attributes['class'] .= ' column-' . $matches[1]; 777 $this->itemcell->attributes['class'] .= ' column-' . $matches[1]; 778 } 779 } 780 } 781 782 /** 783 * Class grade_edit_tree_column_name 784 * 785 * @package core_grades 786 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 787 */ 788 class grade_edit_tree_column_name extends grade_edit_tree_column { 789 public $forced = false; 790 public $hidden = false; 791 public $forced_hidden = false; 792 public $advanced_hidden = false; 793 public $deepest_level = 1; 794 public $hide_when_moving = false; 795 796 public function __construct($params) { 797 if (empty($params['deepest_level'])) { 798 throw new Exception('Tried to instantiate a grade_edit_tree_column_name object without the "deepest_level" param!'); 799 } 800 801 $this->deepest_level = $params['deepest_level']; 802 parent::__construct(); 803 } 804 805 public function get_header_cell() { 806 $headercell = clone($this->headercell); 807 $headercell->colspan = $this->deepest_level + 1; 808 $headercell->text = get_string('name'); 809 return $headercell; 810 } 811 812 public function get_category_cell($category, $levelclass, $params) { 813 global $OUTPUT; 814 815 if (empty($params['name']) || empty($params['level'])) { 816 throw new Exception('Array key (name or level) missing from 3rd param of grade_edit_tree_column_name::get_category_cell($category, $levelclass, $params)'); 817 } 818 $visibilitytoggle = $OUTPUT->render_from_template('core_grades/grade_category_visibility_toggle', [ 819 'category' => $params['eid'] 820 ]); 821 822 $moveaction = isset($params['moveaction']) ? $params['moveaction'] : ''; 823 $categorycell = parent::get_category_cell($category, $levelclass, $params); 824 $categorycell->colspan = ($this->deepest_level + 2) - $params['level']; 825 $categorycell->text = html_writer::div($visibilitytoggle . $moveaction . $params['name'], 'font-weight-bold'); 826 return $categorycell; 827 } 828 829 public function get_item_cell($item, $params) { 830 global $CFG; 831 832 if (empty($params['element']) || empty($params['name']) || empty($params['level'])) { 833 throw new Exception('Array key (name, level or element) missing from 2nd param of grade_edit_tree_column_name::get_item_cell($item, $params)'); 834 } 835 836 $itemicon = \html_writer::div($params['icon'], 'mr-1'); 837 $itemtype = \html_writer::span($params['type'], 'd-block text-uppercase small dimmed_text'); 838 839 // Generate the content for a cell that represents a grade item. 840 // If a behat test site is running avoid outputting the information about the type of the grade item. 841 // This additional information causes issues in behat particularly with the existing xpath used to 842 // interact with table elements. 843 if (!defined('BEHAT_SITE_RUNNING')) { 844 $content = \html_writer::div($itemtype . $params['name']); 845 } else { 846 $content = \html_writer::div($params['name']); 847 } 848 849 $moveaction = isset($params['moveaction']) ? $params['moveaction'] : ''; 850 851 $itemcell = parent::get_item_cell($item, $params); 852 $itemcell->colspan = ($this->deepest_level + 1) - $params['level']; 853 $itemcell->text = \html_writer::div($moveaction . $itemicon . $content, "{$params['itemtype']} d-flex align-items-center"); 854 return $itemcell; 855 } 856 } 857 858 /** 859 * Class grade_edit_tree_column_weight 860 * 861 * @package core_grades 862 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 863 */ 864 class grade_edit_tree_column_weight extends grade_edit_tree_column { 865 866 public function get_header_cell() { 867 global $OUTPUT; 868 $headercell = clone($this->headercell); 869 $headercell->text = get_string('weights', 'grades').$OUTPUT->help_icon('aggregationcoefweight', 'grades'); 870 return $headercell; 871 } 872 873 public function get_category_cell($category, $levelclass, $params) { 874 875 $item = $category->get_grade_item(); 876 $categorycell = parent::get_category_cell($category, $levelclass, $params); 877 $categorycell->text = grade_edit_tree::get_weight_input($item); 878 return $categorycell; 879 } 880 881 public function get_item_cell($item, $params) { 882 global $CFG; 883 if (empty($params['element'])) { 884 throw new Exception('Array key (element) missing from 2nd param of grade_edit_tree_column_weightorextracredit::get_item_cell($item, $params)'); 885 } 886 $itemcell = parent::get_item_cell($item, $params); 887 $itemcell->text = ' '; 888 $object = $params['element']['object']; 889 890 if (!in_array($object->itemtype, array('courseitem', 'categoryitem', 'category')) 891 && !in_array($object->gradetype, array(GRADE_TYPE_NONE, GRADE_TYPE_TEXT)) 892 && (!$object->is_outcome_item() || $object->load_parent_category()->aggregateoutcomes) 893 && ($object->gradetype != GRADE_TYPE_SCALE || !empty($CFG->grade_includescalesinaggregation))) { 894 $itemcell->text = grade_edit_tree::get_weight_input($item); 895 } 896 897 return $itemcell; 898 } 899 } 900 901 /** 902 * Class grade_edit_tree_column_range 903 * 904 * @package core_grades 905 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 906 */ 907 class grade_edit_tree_column_range extends grade_edit_tree_column { 908 909 public function get_header_cell() { 910 $headercell = clone($this->headercell); 911 $headercell->text = get_string('maxgrade', 'grades'); 912 return $headercell; 913 } 914 915 public function get_category_cell($category, $levelclass, $params) { 916 $categorycell = parent::get_category_cell($category, $levelclass, $params); 917 $categorycell->text = ''; 918 return $categorycell; 919 } 920 921 public function get_item_cell($item, $params) { 922 global $DB, $OUTPUT; 923 924 // If the parent aggregation is Natural, we should show the number, even for scales, as that value is used... 925 // ...in the computation. For text grades, the grademax is not used, so we can still show the no value string. 926 $parentcat = $item->get_parent_category(); 927 if ($item->gradetype == GRADE_TYPE_TEXT) { 928 $grademax = ' - '; 929 } else if ($item->gradetype == GRADE_TYPE_SCALE) { 930 $scale = $DB->get_record('scale', array('id' => $item->scaleid)); 931 $scale_items = null; 932 if (empty($scale)) { //if the item is using a scale that's been removed 933 $scale_items = array(); 934 } else { 935 $scale_items = explode(',', $scale->scale); 936 } 937 if ($parentcat->aggregation == GRADE_AGGREGATE_SUM) { 938 $grademax = end($scale_items) . ' (' . 939 format_float($item->grademax, $item->get_decimals()) . ')'; 940 } else { 941 $grademax = end($scale_items) . ' (' . count($scale_items) . ')'; 942 } 943 } else { 944 $grademax = format_float($item->grademax, $item->get_decimals()); 945 } 946 947 $isextracredit = false; 948 if ($item->aggregationcoef > 0) { 949 // For category grade items, we need the grandparent category. 950 // The parent is just category the grade item represents. 951 if ($item->is_category_item()) { 952 $grandparentcat = $parentcat->get_parent_category(); 953 if ($grandparentcat->is_extracredit_used()) { 954 $isextracredit = true; 955 } 956 } else if ($parentcat->is_extracredit_used()) { 957 $isextracredit = true; 958 } 959 } 960 if ($isextracredit) { 961 $grademax .= ' ' . html_writer::tag('abbr', get_string('aggregationcoefextrasumabbr', 'grades'), 962 array('title' => get_string('aggregationcoefextrasum', 'grades'))); 963 } 964 965 $itemcell = parent::get_item_cell($item, $params); 966 $itemcell->text = $grademax; 967 return $itemcell; 968 } 969 } 970 971 /** 972 * Class grade_edit_tree_column_status 973 * 974 * @package core_grades 975 * @copyright 2023 Ilya Tregubov <ilya@moodle.com> 976 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 977 */ 978 class grade_edit_tree_column_status extends grade_edit_tree_column { 979 980 /** 981 * Get status column header cell 982 * @return html_table_cell status column header cell 983 */ 984 public function get_header_cell() { 985 $headercell = clone($this->headercell); 986 $headercell->text = get_string('status'); 987 return $headercell; 988 } 989 990 /** 991 * Get category cell in status column 992 * 993 * @param grade_category $category grade category 994 * @param string $levelclass Category level info 995 * @param array $params Params (category id, action performed etc) 996 * @return html_table_cell category cell in status columns 997 */ 998 public function get_category_cell($category, $levelclass, $params) { 999 global $OUTPUT, $gtree; 1000 $categorycell = parent::get_category_cell($category, $levelclass, $params); 1001 $element = []; 1002 $element['object'] = $category; 1003 $categorycell->text = $gtree->set_grade_status_icons($element); 1004 1005 // Aggregation type. 1006 $aggrstrings = grade_helper::get_aggregation_strings(); 1007 $context = new stdClass(); 1008 $context->aggregation = $aggrstrings[$category->aggregation]; 1009 1010 // Include/exclude empty grades. 1011 if ($category->aggregateonlygraded) { 1012 $context->aggregateonlygraded = $category->aggregateonlygraded; 1013 } 1014 1015 // Aggregate outcomes. 1016 if ($category->aggregateoutcomes) { 1017 $context->aggregateoutcomes = $category->aggregateoutcomes; 1018 } 1019 1020 // Drop the lowest. 1021 if ($category->droplow) { 1022 $context->droplow = $category->droplow; 1023 } 1024 1025 // Keep the highest. 1026 if ($category->keephigh) { 1027 $context->keephigh = $category->keephigh; 1028 } 1029 $categorycell->text .= $OUTPUT->render_from_template('core_grades/category_settings', $context); 1030 return $categorycell; 1031 } 1032 1033 /** 1034 * Get category cell in status column 1035 * 1036 * @param grade_item $item grade item 1037 * @param array $params Params 1038 * @return html_table_cell item cell in status columns 1039 */ 1040 public function get_item_cell($item, $params) { 1041 global $gtree; 1042 1043 $element = []; 1044 $element['object'] = $item; 1045 $itemcell = parent::get_item_cell($item, $params); 1046 $itemcell->text = $gtree->set_grade_status_icons($element); 1047 return $itemcell; 1048 } 1049 } 1050 1051 /** 1052 * Class grade_edit_tree_column_actions 1053 * 1054 * @package core_grades 1055 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 1056 */ 1057 class grade_edit_tree_column_actions extends grade_edit_tree_column { 1058 1059 public function __construct($params) { 1060 parent::__construct(); 1061 } 1062 1063 public function get_header_cell() { 1064 $headercell = clone($this->headercell); 1065 $headercell->text = get_string('actions'); 1066 return $headercell; 1067 } 1068 1069 public function get_category_cell($category, $levelclass, $params) { 1070 1071 if (empty($params['actions'])) { 1072 throw new Exception('Array key (actions) missing from 3rd param of grade_edit_tree_column_actions::get_category_actions($category, $levelclass, $params)'); 1073 } 1074 1075 $categorycell = parent::get_category_cell($category, $levelclass, $params); 1076 $categorycell->text = $params['actions']; 1077 return $categorycell; 1078 } 1079 1080 public function get_item_cell($item, $params) { 1081 if (empty($params['actions'])) { 1082 throw new Exception('Array key (actions) missing from 2nd param of grade_edit_tree_column_actions::get_item_cell($item, $params)'); 1083 } 1084 $itemcell = parent::get_item_cell($item, $params); 1085 $itemcell->text = $params['actions']; 1086 return $itemcell; 1087 } 1088 } 1089 1090 /** 1091 * Class grade_edit_tree_column_select 1092 * 1093 * @package core_grades 1094 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 1095 */ 1096 class grade_edit_tree_column_select extends grade_edit_tree_column { 1097 1098 public function get_header_cell() { 1099 $headercell = clone($this->headercell); 1100 $headercell->text = get_string('select'); 1101 return $headercell; 1102 } 1103 1104 public function get_category_cell($category, $levelclass, $params) { 1105 global $OUTPUT; 1106 1107 if (empty($params['eid'])) { 1108 throw new Exception('Array key (eid) missing from 3rd param of grade_edit_tree_column_select::get_category_cell($category, $levelclass, $params)'); 1109 } 1110 1111 // Get toggle group for this master checkbox. 1112 $togglegroup = $this->get_checkbox_togglegroup($category); 1113 // Set label for this master checkbox. 1114 $masterlabel = get_string('all'); 1115 // Use category name if available. 1116 if ($category->fullname !== '?') { 1117 $masterlabel = format_string($category->fullname, true, ['escape' => false]); 1118 // Limit the displayed category name to prevent the Select column from getting too wide. 1119 if (core_text::strlen($masterlabel) > 20) { 1120 $masterlabel = get_string('textellipsis', 'core', core_text::substr($masterlabel, 0, 12)); 1121 } 1122 } 1123 // Build the master checkbox. 1124 $mastercheckbox = new \core\output\checkbox_toggleall($togglegroup, true, [ 1125 'id' => 'select_category_' . $category->id, 1126 'name' => $togglegroup, 1127 'value' => 1, 1128 'classes' => 'itemselect ignoredirty', 1129 'label' => $masterlabel, 1130 // Consistent label to prevent the select column from resizing. 1131 'selectall' => $masterlabel, 1132 'deselectall' => $masterlabel, 1133 'labelclasses' => 'm-0', 1134 ]); 1135 1136 $categorycell = parent::get_category_cell($category, $levelclass, $params); 1137 $categorycell->text = $OUTPUT->render($mastercheckbox); 1138 return $categorycell; 1139 } 1140 1141 public function get_item_cell($item, $params) { 1142 if (empty($params['itemtype']) || empty($params['eid'])) { 1143 throw new \moodle_exception('missingitemtypeoreid', 'core_grades'); 1144 } 1145 $itemcell = parent::get_item_cell($item, $params); 1146 1147 if ($params['itemtype'] != 'course' && $params['itemtype'] != 'category') { 1148 global $OUTPUT; 1149 1150 // Fetch the grade item's category. 1151 $category = grade_category::fetch(['id' => $item->categoryid]); 1152 $togglegroup = $this->get_checkbox_togglegroup($category); 1153 1154 $checkboxid = 'select_' . $params['eid']; 1155 $checkbox = new \core\output\checkbox_toggleall($togglegroup, false, [ 1156 'id' => $checkboxid, 1157 'name' => $checkboxid, 1158 'label' => get_string('select', 'grades', $item->itemname), 1159 'labelclasses' => 'accesshide', 1160 'classes' => 'itemselect ignoredirty', 1161 ]); 1162 $itemcell->text = $OUTPUT->render($checkbox); 1163 } 1164 return $itemcell; 1165 } 1166 1167 /** 1168 * Generates a toggle group name for a bulk-action checkbox based on the given grade category. 1169 * 1170 * @param grade_category $category The grade category. 1171 * @return string 1172 */ 1173 protected function get_checkbox_togglegroup(grade_category $category): string { 1174 $levels = []; 1175 $categories = explode('/', $category->path); 1176 foreach ($categories as $categoryid) { 1177 $level = 'category' . $categoryid; 1178 if (!in_array($level, $levels)) { 1179 $levels[] = 'category' . $categoryid; 1180 } 1181 } 1182 $togglegroup = implode(' ', $levels); 1183 1184 return $togglegroup; 1185 } 1186 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body