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