Differences Between: [Versions 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 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 * Advanced grading methods support 19 * 20 * @package core_grading 21 * @copyright 2011 David Mudrak <david@moodle.com> 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 25 defined('MOODLE_INTERNAL') || die(); 26 27 use core_grades\component_gradeitems; 28 29 /** 30 * Factory method returning an instance of the grading manager 31 * 32 * There are basically ways how to use this factory method. If the area record 33 * id is known to the caller, get the manager for that area by providing just 34 * the id. If the area record id is not know, the context, component and area name 35 * can be provided. Note that null values are allowed in the second case as the context, 36 * component and the area name can be set explicitly later. 37 * 38 * @category grading 39 * @example $manager = get_grading_manager($areaid); 40 * @example $manager = get_grading_manager(context_system::instance()); 41 * @example $manager = get_grading_manager($context, 'mod_assignment', 'submission'); 42 * @param stdClass|int|null $context_or_areaid if $areaid is passed, no other parameter is needed 43 * @param string|null $component the frankenstyle name of the component 44 * @param string|null $area the name of the gradable area 45 * @return grading_manager 46 */ 47 function get_grading_manager($context_or_areaid = null, $component = null, $area = null) { 48 global $DB; 49 50 $manager = new grading_manager(); 51 52 if (is_object($context_or_areaid)) { 53 $context = $context_or_areaid; 54 } else { 55 $context = null; 56 57 if (is_numeric($context_or_areaid)) { 58 $manager->load($context_or_areaid); 59 return $manager; 60 } 61 } 62 63 if (!is_null($context)) { 64 $manager->set_context($context); 65 } 66 67 if (!is_null($component)) { 68 $manager->set_component($component); 69 } 70 71 if (!is_null($area)) { 72 $manager->set_area($area); 73 } 74 75 return $manager; 76 } 77 78 /** 79 * General class providing access to common grading features 80 * 81 * Grading manager provides access to the particular grading method controller 82 * in that area. 83 * 84 * Fully initialized instance of the grading manager operates over a single 85 * gradable area. It is possible to work with a partially initialized manager 86 * that knows just context and component without known area, for example. 87 * It is also possible to change context, component and area of an existing 88 * manager. Such pattern is used when copying form definitions, for example. 89 * 90 * @package core_grading 91 * @copyright 2011 David Mudrak <david@moodle.com> 92 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 93 * @category grading 94 */ 95 class grading_manager { 96 97 /** @var stdClass the context */ 98 protected $context; 99 100 /** @var string the frankenstyle name of the component */ 101 protected $component; 102 103 /** @var string the name of the gradable area */ 104 protected $area; 105 106 /** @var stdClass|false|null the raw record from {grading_areas}, false if does not exist, null if invalidated cache */ 107 private $areacache = null; 108 109 /** 110 * Returns grading manager context 111 * 112 * @return stdClass grading manager context 113 */ 114 public function get_context() { 115 return $this->context; 116 } 117 118 /** 119 * Sets the context the manager operates on 120 * 121 * @param stdClass $context 122 */ 123 public function set_context(stdClass $context) { 124 $this->areacache = null; 125 $this->context = $context; 126 } 127 128 /** 129 * Returns grading manager component 130 * 131 * @return string grading manager component 132 */ 133 public function get_component() { 134 return $this->component; 135 } 136 137 /** 138 * Sets the component the manager operates on 139 * 140 * @param string $component the frankenstyle name of the component 141 */ 142 public function set_component($component) { 143 $this->areacache = null; 144 list($type, $name) = core_component::normalize_component($component); 145 $this->component = $type.'_'.$name; 146 } 147 148 /** 149 * Returns grading manager area name 150 * 151 * @return string grading manager area name 152 */ 153 public function get_area() { 154 return $this->area; 155 } 156 157 /** 158 * Sets the area the manager operates on 159 * 160 * @param string $area the name of the gradable area 161 */ 162 public function set_area($area) { 163 $this->areacache = null; 164 $this->area = $area; 165 } 166 167 /** 168 * Returns a text describing the context and the component 169 * 170 * At the moment this works for gradable areas in course modules. In the future, this 171 * method should be improved so it works for other contexts (blocks, gradebook items etc) 172 * or subplugins. 173 * 174 * @return string 175 */ 176 public function get_component_title() { 177 178 $this->ensure_isset(array('context', 'component')); 179 180 if ($this->get_context()->contextlevel == CONTEXT_SYSTEM) { 181 if ($this->get_component() == 'core_grading') { 182 $title = ''; // we are in the bank UI 183 } else { 184 throw new coding_exception('Unsupported component at the system context'); 185 } 186 187 } else if ($this->get_context()->contextlevel >= CONTEXT_COURSE) { 188 list($context, $course, $cm) = get_context_info_array($this->get_context()->id); 189 190 if ($cm && strval($cm->name) !== '') { 191 $title = format_string($cm->name, true, array('context' => $context)); 192 } else { 193 debugging('Gradable areas are currently supported at the course module level only', DEBUG_DEVELOPER); 194 $title = $this->get_component(); 195 } 196 197 } else { 198 throw new coding_exception('Unsupported gradable area context level'); 199 } 200 201 return $title; 202 } 203 204 /** 205 * Returns the localized title of the currently set area 206 * 207 * @return string 208 */ 209 public function get_area_title() { 210 211 if ($this->get_context()->contextlevel == CONTEXT_SYSTEM) { 212 return ''; 213 214 } else if ($this->get_context()->contextlevel >= CONTEXT_COURSE) { 215 $this->ensure_isset(array('context', 'component', 'area')); 216 $areas = $this->get_available_areas(); 217 if (array_key_exists($this->get_area(), $areas)) { 218 return $areas[$this->get_area()]; 219 } else { 220 debugging('Unknown area!'); 221 return '???'; 222 } 223 224 } else { 225 throw new coding_exception('Unsupported context level'); 226 } 227 } 228 229 /** 230 * Loads the gradable area info from the database 231 * 232 * @param int $areaid 233 */ 234 public function load($areaid) { 235 global $DB; 236 237 $this->areacache = $DB->get_record('grading_areas', array('id' => $areaid), '*', MUST_EXIST); 238 $this->context = context::instance_by_id($this->areacache->contextid, MUST_EXIST); 239 $this->component = $this->areacache->component; 240 $this->area = $this->areacache->areaname; 241 } 242 243 /** 244 * Returns the list of installed grading plugins together, optionally extended 245 * with a simple direct grading. 246 * 247 * @param bool $includenone should the 'Simple direct grading' be included 248 * @return array of the (string)name => (string)localized title of the method 249 */ 250 public static function available_methods($includenone = true) { 251 252 if ($includenone) { 253 $list = array('' => get_string('gradingmethodnone', 'core_grading')); 254 } else { 255 $list = array(); 256 } 257 258 foreach (core_component::get_plugin_list('gradingform') as $name => $location) { 259 $list[$name] = get_string('pluginname', 'gradingform_'.$name); 260 } 261 262 return $list; 263 } 264 265 /** 266 * Returns the list of available grading methods in the given context 267 * 268 * Currently this is just a static list obtained from {@link self::available_methods()}. 269 * In the future, the list of available methods may be controlled per-context. 270 * 271 * Requires the context property to be set in advance. 272 * 273 * @param bool $includenone should the 'Simple direct grading' be included 274 * @return array of the (string)name => (string)localized title of the method 275 */ 276 public function get_available_methods($includenone = true) { 277 $this->ensure_isset(array('context')); 278 return self::available_methods($includenone); 279 } 280 281 /** 282 * Returns the list of gradable areas provided by the given component 283 * 284 * This performs a callback to the library of the relevant plugin to obtain 285 * the list of supported areas. 286 * 287 * @param string $component normalized component name 288 * @return array of (string)areacode => (string)localized title of the area 289 */ 290 public static function available_areas($component) { 291 global $CFG; 292 293 if (component_gradeitems::defines_advancedgrading_itemnames_for_component($component)) { 294 $result = []; 295 foreach (component_gradeitems::get_advancedgrading_itemnames_for_component($component) as $itemnumber => $itemname) { 296 $result[$itemname] = get_string("gradeitem:{$itemname}", $component); 297 } 298 299 return $result; 300 } 301 302 list($plugintype, $pluginname) = core_component::normalize_component($component); 303 304 if ($component === 'core_grading') { 305 return array(); 306 307 } else if ($plugintype === 'mod') { 308 $callbackfunction = "grading_areas_list"; 309 if (component_callback_exists($component, $callbackfunction)) { 310 debugging( 311 "Components supporting advanced grading should be updated to implement the component_gradeitems class", 312 DEBUG_DEVELOPER 313 ); 314 return component_callback($component, $callbackfunction, [], []); 315 } 316 } else { 317 throw new coding_exception('Unsupported area location'); 318 } 319 } 320 321 322 /** 323 * Returns the list of gradable areas in the given context and component 324 * 325 * This performs a callback to the library of the relevant plugin to obtain 326 * the list of supported areas. 327 * @return array of (string)areacode => (string)localized title of the area 328 */ 329 public function get_available_areas() { 330 global $CFG; 331 332 $this->ensure_isset(array('context', 'component')); 333 334 if ($this->get_context()->contextlevel == CONTEXT_SYSTEM) { 335 if ($this->get_component() !== 'core_grading') { 336 throw new coding_exception('Unsupported component at the system context'); 337 } else { 338 return array(); 339 } 340 341 } else if ($this->get_context()->contextlevel == CONTEXT_MODULE) { 342 $modulecontext = $this->get_context(); 343 $coursecontext = $modulecontext->get_course_context(); 344 $cm = get_fast_modinfo($coursecontext->instanceid)->get_cm($modulecontext->instanceid); 345 return self::available_areas("mod_{$cm->modname}"); 346 347 } else { 348 throw new coding_exception('Unsupported gradable area context level'); 349 } 350 } 351 352 /** 353 * Returns the currently active grading method in the gradable area 354 * 355 * @return string|null the name of the grading plugin of null if it has not been set 356 */ 357 public function get_active_method() { 358 global $DB; 359 360 $this->ensure_isset(array('context', 'component', 'area')); 361 362 // get the current grading area record if it exists 363 if (is_null($this->areacache)) { 364 $this->areacache = $DB->get_record('grading_areas', array( 365 'contextid' => $this->context->id, 366 'component' => $this->component, 367 'areaname' => $this->area), 368 '*', IGNORE_MISSING); 369 } 370 371 if ($this->areacache === false) { 372 // no area record yet 373 return null; 374 } 375 376 return $this->areacache->activemethod; 377 } 378 379 /** 380 * Sets the currently active grading method in the gradable area 381 * 382 * @param string $method the method name, eg 'rubric' (must be available) 383 * @return bool true if the method changed or was just set, false otherwise 384 */ 385 public function set_active_method($method) { 386 global $DB; 387 388 $this->ensure_isset(array('context', 'component', 'area')); 389 390 // make sure the passed method is empty or a valid plugin name 391 if (empty($method)) { 392 $method = null; 393 } else { 394 if ('gradingform_'.$method !== clean_param('gradingform_'.$method, PARAM_COMPONENT)) { 395 throw new moodle_exception('invalid_method_name', 'core_grading'); 396 } 397 $available = $this->get_available_methods(false); 398 if (!array_key_exists($method, $available)) { 399 throw new moodle_exception('invalid_method_name', 'core_grading'); 400 } 401 } 402 403 // get the current grading area record if it exists 404 if (is_null($this->areacache)) { 405 $this->areacache = $DB->get_record('grading_areas', array( 406 'contextid' => $this->context->id, 407 'component' => $this->component, 408 'areaname' => $this->area), 409 '*', IGNORE_MISSING); 410 } 411 412 $methodchanged = false; 413 414 if ($this->areacache === false) { 415 // no area record yet, create one with the active method set 416 $area = array( 417 'contextid' => $this->context->id, 418 'component' => $this->component, 419 'areaname' => $this->area, 420 'activemethod' => $method); 421 $DB->insert_record('grading_areas', $area); 422 $methodchanged = true; 423 424 } else { 425 // update the existing record if needed 426 if ($this->areacache->activemethod !== $method) { 427 $DB->set_field('grading_areas', 'activemethod', $method, array('id' => $this->areacache->id)); 428 $methodchanged = true; 429 } 430 } 431 432 $this->areacache = null; 433 434 return $methodchanged; 435 } 436 437 /** 438 * Extends the settings navigation with the grading settings 439 * 440 * This function is called when the context for the page is an activity module with the 441 * FEATURE_ADVANCED_GRADING and the user has the permission moodle/grade:managegradingforms. 442 * 443 * @param settings_navigation $settingsnav {@link settings_navigation} 444 * @param navigation_node $modulenode {@link navigation_node} 445 */ 446 public function extend_settings_navigation(settings_navigation $settingsnav, navigation_node $modulenode=null) { 447 448 $this->ensure_isset(array('context', 'component')); 449 450 $areas = $this->get_available_areas(); 451 452 if (empty($areas)) { 453 // no money, no funny 454 return; 455 456 } else if (count($areas) == 1) { 457 // make just a single node for the management screen 458 $areatitle = reset($areas); 459 $areaname = key($areas); 460 $this->set_area($areaname); 461 $method = $this->get_active_method(); 462 $managementnode = $modulenode->add(get_string('gradingmanagement', 'core_grading'), 463 $this->get_management_url(), settings_navigation::TYPE_CUSTOM); 464 if ($method) { 465 $controller = $this->get_controller($method); 466 $controller->extend_settings_navigation($settingsnav, $managementnode); 467 } 468 469 } else { 470 // make management screen node for each area 471 $managementnode = $modulenode->add(get_string('gradingmanagement', 'core_grading'), 472 null, settings_navigation::TYPE_CUSTOM); 473 foreach ($areas as $areaname => $areatitle) { 474 $this->set_area($areaname); 475 $method = $this->get_active_method(); 476 $node = $managementnode->add($areatitle, 477 $this->get_management_url(), settings_navigation::TYPE_CUSTOM); 478 if ($method) { 479 $controller = $this->get_controller($method); 480 $controller->extend_settings_navigation($settingsnav, $node); 481 } 482 } 483 } 484 } 485 486 /** 487 * Extends the module navigation with the advanced grading information 488 * 489 * This function is called when the context for the page is an activity module with the 490 * FEATURE_ADVANCED_GRADING. 491 * 492 * @param global_navigation $navigation 493 * @param navigation_node $modulenode 494 */ 495 public function extend_navigation(global_navigation $navigation, navigation_node $modulenode=null) { 496 $this->ensure_isset(array('context', 'component')); 497 498 $areas = $this->get_available_areas(); 499 foreach ($areas as $areaname => $areatitle) { 500 $this->set_area($areaname); 501 if ($controller = $this->get_active_controller()) { 502 $controller->extend_navigation($navigation, $modulenode); 503 } 504 } 505 } 506 507 /** 508 * Returns the given method's controller in the gradable area 509 * 510 * @param string $method the method name, eg 'rubric' (must be available) 511 * @return gradingform_controller 512 */ 513 public function get_controller($method) { 514 global $CFG, $DB; 515 516 $this->ensure_isset(array('context', 'component', 'area')); 517 518 // make sure the passed method is a valid plugin name 519 if ('gradingform_'.$method !== clean_param('gradingform_'.$method, PARAM_COMPONENT)) { 520 throw new moodle_exception('invalid_method_name', 'core_grading'); 521 } 522 $available = $this->get_available_methods(false); 523 if (!array_key_exists($method, $available)) { 524 throw new moodle_exception('invalid_method_name', 'core_grading'); 525 } 526 527 // get the current grading area record if it exists 528 if (is_null($this->areacache)) { 529 $this->areacache = $DB->get_record('grading_areas', array( 530 'contextid' => $this->context->id, 531 'component' => $this->component, 532 'areaname' => $this->area), 533 '*', IGNORE_MISSING); 534 } 535 536 if ($this->areacache === false) { 537 // no area record yet, create one 538 $area = array( 539 'contextid' => $this->context->id, 540 'component' => $this->component, 541 'areaname' => $this->area); 542 $areaid = $DB->insert_record('grading_areas', $area); 543 // reload the cache 544 $this->areacache = $DB->get_record('grading_areas', array('id' => $areaid), '*', MUST_EXIST); 545 } 546 547 require_once($CFG->dirroot.'/grade/grading/form/'.$method.'/lib.php'); 548 $classname = 'gradingform_'.$method.'_controller'; 549 550 return new $classname($this->context, $this->component, $this->area, $this->areacache->id); 551 } 552 553 /** 554 * Returns the controller for the active method if it is available 555 * 556 * @return null|gradingform_controller 557 */ 558 public function get_active_controller() { 559 if ($gradingmethod = $this->get_active_method()) { 560 $controller = $this->get_controller($gradingmethod); 561 if ($controller->is_form_available()) { 562 return $controller; 563 } 564 } 565 return null; 566 } 567 568 /** 569 * Returns the URL of the grading area management page 570 * 571 * @param moodle_url $returnurl optional URL of the page where the user should be sent back to 572 * @return moodle_url 573 */ 574 public function get_management_url(moodle_url $returnurl = null) { 575 576 $this->ensure_isset(array('context', 'component')); 577 578 if ($this->areacache) { 579 $params = array('areaid' => $this->areacache->id); 580 } else { 581 $params = array('contextid' => $this->context->id, 'component' => $this->component); 582 if ($this->area) { 583 $params['area'] = $this->area; 584 } 585 } 586 587 if (!is_null($returnurl)) { 588 $params['returnurl'] = $returnurl->out(false); 589 } 590 591 return new moodle_url('/grade/grading/manage.php', $params); 592 } 593 594 /** 595 * Creates a new shared area to hold a grading form template 596 * 597 * Shared area are implemented as virtual gradable areas at the system level context 598 * with the component set to core_grading and unique random area name. 599 * 600 * @param string $method the name of the plugin we create the area for 601 * @return int the new area id 602 */ 603 public function create_shared_area($method) { 604 global $DB; 605 606 // generate some unique random name for the new area 607 $name = $method . '_' . sha1(rand().uniqid($method, true)); 608 // create new area record 609 $area = array( 610 'contextid' => context_system::instance()->id, 611 'component' => 'core_grading', 612 'areaname' => $name, 613 'activemethod' => $method); 614 return $DB->insert_record('grading_areas', $area); 615 } 616 617 /** 618 * Removes all data associated with the given context 619 * 620 * This is called by {@link context::delete_content()} 621 * 622 * @param int $contextid context id 623 */ 624 public static function delete_all_for_context($contextid) { 625 global $DB; 626 627 $areaids = $DB->get_fieldset_select('grading_areas', 'id', 'contextid = ?', array($contextid)); 628 $methods = array_keys(self::available_methods(false)); 629 630 foreach($areaids as $areaid) { 631 $manager = get_grading_manager($areaid); 632 foreach ($methods as $method) { 633 $controller = $manager->get_controller($method); 634 $controller->delete_definition(); 635 } 636 } 637 638 $DB->delete_records_list('grading_areas', 'id', $areaids); 639 } 640 641 /** 642 * Helper method to tokenize the given string 643 * 644 * Splits the given string into smaller strings. This is a helper method for 645 * full text searching in grading forms. If the given string is surrounded with 646 * double quotes, the resulting array consists of a single item containing the 647 * quoted content. 648 * 649 * Otherwise, string like 'grammar, english language' would be tokenized into 650 * the three tokens 'grammar', 'english', 'language'. 651 * 652 * One-letter tokens like are dropped in non-phrase mode. Repeated tokens are 653 * returned just once. 654 * 655 * @param string $needle 656 * @return array 657 */ 658 public static function tokenize($needle) { 659 660 // check if we are searching for the exact phrase 661 if (preg_match('/^[\s]*"[\s]*(.*?)[\s]*"[\s]*$/', $needle, $matches)) { 662 $token = $matches[1]; 663 if ($token === '') { 664 return array(); 665 } else { 666 return array($token); 667 } 668 } 669 670 // split the needle into smaller parts separated by non-word characters 671 $tokens = preg_split("/\W/u", $needle); 672 // keep just non-empty parts 673 $tokens = array_filter($tokens); 674 // distinct 675 $tokens = array_unique($tokens); 676 // drop one-letter tokens 677 foreach ($tokens as $ix => $token) { 678 if (strlen($token) == 1) { 679 unset($tokens[$ix]); 680 } 681 } 682 683 return array_values($tokens); 684 } 685 686 // ////////////////////////////////////////////////////////////////////////// 687 688 /** 689 * Make sure that the given properties were set to some not-null value 690 * 691 * @param array $properties the list of properties 692 * @throws coding_exception 693 */ 694 private function ensure_isset(array $properties) { 695 foreach ($properties as $property) { 696 if (!isset($this->$property)) { 697 throw new coding_exception('The property "'.$property.'" is not set.'); 698 } 699 } 700 } 701 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body