Differences Between: [Versions 310 and 402] [Versions 310 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 * Grading method controller for the Rubric plugin 19 * 20 * @package gradingform_rubric 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 require_once($CFG->dirroot.'/grade/grading/form/lib.php'); 28 require_once($CFG->dirroot.'/lib/filelib.php'); 29 30 /** rubric: Used to compare our gradeitem_type against. */ 31 const RUBRIC = 'rubric'; 32 33 /** 34 * This controller encapsulates the rubric grading logic 35 * 36 * @package gradingform_rubric 37 * @copyright 2011 David Mudrak <david@moodle.com> 38 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 39 */ 40 class gradingform_rubric_controller extends gradingform_controller { 41 // Modes of displaying the rubric (used in gradingform_rubric_renderer) 42 /** Rubric display mode: For editing (moderator or teacher creates a rubric) */ 43 const DISPLAY_EDIT_FULL = 1; 44 /** Rubric display mode: Preview the rubric design with hidden fields */ 45 const DISPLAY_EDIT_FROZEN = 2; 46 /** Rubric display mode: Preview the rubric design (for person with manage permission) */ 47 const DISPLAY_PREVIEW = 3; 48 /** Rubric display mode: Preview the rubric (for people being graded) */ 49 const DISPLAY_PREVIEW_GRADED= 8; 50 /** Rubric display mode: For evaluation, enabled (teacher grades a student) */ 51 const DISPLAY_EVAL = 4; 52 /** Rubric display mode: For evaluation, with hidden fields */ 53 const DISPLAY_EVAL_FROZEN = 5; 54 /** Rubric display mode: Teacher reviews filled rubric */ 55 const DISPLAY_REVIEW = 6; 56 /** Rubric display mode: Dispaly filled rubric (i.e. students see their grades) */ 57 const DISPLAY_VIEW = 7; 58 59 /** 60 * Extends the module settings navigation with the rubric grading settings 61 * 62 * This function is called when the context for the page is an activity module with the 63 * FEATURE_ADVANCED_GRADING, the user has the permission moodle/grade:managegradingforms 64 * and there is an area with the active grading method set to 'rubric'. 65 * 66 * @param settings_navigation $settingsnav {@link settings_navigation} 67 * @param navigation_node $node {@link navigation_node} 68 */ 69 public function extend_settings_navigation(settings_navigation $settingsnav, navigation_node $node=null) { 70 $node->add(get_string('definerubric', 'gradingform_rubric'), 71 $this->get_editor_url(), settings_navigation::TYPE_CUSTOM, 72 null, null, new pix_icon('icon', '', 'gradingform_rubric')); 73 } 74 75 /** 76 * Extends the module navigation 77 * 78 * This function is called when the context for the page is an activity module with the 79 * FEATURE_ADVANCED_GRADING and there is an area with the active grading method set to the given plugin. 80 * 81 * @param global_navigation $navigation {@link global_navigation} 82 * @param navigation_node $node {@link navigation_node} 83 */ 84 public function extend_navigation(global_navigation $navigation, navigation_node $node=null) { 85 if (has_capability('moodle/grade:managegradingforms', $this->get_context())) { 86 // no need for preview if user can manage forms, he will have link to manage.php in settings instead 87 return; 88 } 89 if ($this->is_form_defined() && ($options = $this->get_options()) && !empty($options['alwaysshowdefinition'])) { 90 $node->add(get_string('gradingof', 'gradingform_rubric', get_grading_manager($this->get_areaid())->get_area_title()), 91 new moodle_url('/grade/grading/form/'.$this->get_method_name().'/preview.php', array('areaid' => $this->get_areaid())), 92 settings_navigation::TYPE_CUSTOM); 93 } 94 } 95 96 /** 97 * Saves the rubric definition into the database 98 * 99 * @see parent::update_definition() 100 * @param stdClass $newdefinition rubric definition data as coming from gradingform_rubric_editrubric::get_data() 101 * @param int|null $usermodified optional userid of the author of the definition, defaults to the current user 102 */ 103 public function update_definition(stdClass $newdefinition, $usermodified = null) { 104 $this->update_or_check_rubric($newdefinition, $usermodified, true); 105 if (isset($newdefinition->rubric['regrade']) && $newdefinition->rubric['regrade']) { 106 $this->mark_for_regrade(); 107 } 108 } 109 110 /** 111 * Either saves the rubric definition into the database or check if it has been changed. 112 * Returns the level of changes: 113 * 0 - no changes 114 * 1 - only texts or criteria sortorders are changed, students probably do not require re-grading 115 * 2 - added levels but maximum score on rubric is the same, students still may not require re-grading 116 * 3 - removed criteria or added levels or changed number of points, students require re-grading but may be re-graded automatically 117 * 4 - removed levels - students require re-grading and not all students may be re-graded automatically 118 * 5 - added criteria - all students require manual re-grading 119 * 120 * @param stdClass $newdefinition rubric definition data as coming from gradingform_rubric_editrubric::get_data() 121 * @param int|null $usermodified optional userid of the author of the definition, defaults to the current user 122 * @param boolean $doupdate if true actually updates DB, otherwise performs a check 123 * 124 */ 125 public function update_or_check_rubric(stdClass $newdefinition, $usermodified = null, $doupdate = false) { 126 global $DB; 127 128 // firstly update the common definition data in the {grading_definition} table 129 if ($this->definition === false) { 130 if (!$doupdate) { 131 // if we create the new definition there is no such thing as re-grading anyway 132 return 5; 133 } 134 // if definition does not exist yet, create a blank one 135 // (we need id to save files embedded in description) 136 parent::update_definition(new stdClass(), $usermodified); 137 parent::load_definition(); 138 } 139 if (!isset($newdefinition->rubric['options'])) { 140 $newdefinition->rubric['options'] = self::get_default_options(); 141 } 142 $newdefinition->options = json_encode($newdefinition->rubric['options']); 143 $editoroptions = self::description_form_field_options($this->get_context()); 144 $newdefinition = file_postupdate_standard_editor($newdefinition, 'description', $editoroptions, $this->get_context(), 145 'grading', 'description', $this->definition->id); 146 147 // reload the definition from the database 148 $currentdefinition = $this->get_definition(true); 149 150 $haschanges = array(); 151 152 // Check if 'lockzeropoints' option has changed. 153 $newlockzeropoints = $newdefinition->rubric['options']['lockzeropoints']; 154 $currentoptions = $this->get_options(); 155 if ((bool)$newlockzeropoints != (bool)$currentoptions['lockzeropoints']) { 156 $haschanges[3] = true; 157 } 158 159 // update rubric data 160 if (empty($newdefinition->rubric['criteria'])) { 161 $newcriteria = array(); 162 } else { 163 $newcriteria = $newdefinition->rubric['criteria']; // new ones to be saved 164 } 165 $currentcriteria = $currentdefinition->rubric_criteria; 166 $criteriafields = array('sortorder', 'description', 'descriptionformat'); 167 $levelfields = array('score', 'definition', 'definitionformat'); 168 foreach ($newcriteria as $id => $criterion) { 169 // get list of submitted levels 170 $levelsdata = array(); 171 if (array_key_exists('levels', $criterion)) { 172 $levelsdata = $criterion['levels']; 173 } 174 $criterionmaxscore = null; 175 if (preg_match('/^NEWID\d+$/', $id)) { 176 // insert criterion into DB 177 $data = array('definitionid' => $this->definition->id, 'descriptionformat' => FORMAT_MOODLE); // TODO MDL-31235 format is not supported yet 178 foreach ($criteriafields as $key) { 179 if (array_key_exists($key, $criterion)) { 180 $data[$key] = $criterion[$key]; 181 } 182 } 183 if ($doupdate) { 184 $id = $DB->insert_record('gradingform_rubric_criteria', $data); 185 } 186 $haschanges[5] = true; 187 } else { 188 // update criterion in DB 189 $data = array(); 190 foreach ($criteriafields as $key) { 191 if (array_key_exists($key, $criterion) && $criterion[$key] != $currentcriteria[$id][$key]) { 192 $data[$key] = $criterion[$key]; 193 } 194 } 195 if (!empty($data)) { 196 // update only if something is changed 197 $data['id'] = $id; 198 if ($doupdate) { 199 $DB->update_record('gradingform_rubric_criteria', $data); 200 } 201 $haschanges[1] = true; 202 } 203 // remove deleted levels from DB and calculate the maximum score for this criteria 204 foreach ($currentcriteria[$id]['levels'] as $levelid => $currentlevel) { 205 if ($criterionmaxscore === null || $criterionmaxscore < $currentlevel['score']) { 206 $criterionmaxscore = $currentlevel['score']; 207 } 208 if (!array_key_exists($levelid, $levelsdata)) { 209 if ($doupdate) { 210 $DB->delete_records('gradingform_rubric_levels', array('id' => $levelid)); 211 } 212 $haschanges[4] = true; 213 } 214 } 215 } 216 foreach ($levelsdata as $levelid => $level) { 217 if (isset($level['score'])) { 218 $level['score'] = unformat_float($level['score']); 219 } 220 if (preg_match('/^NEWID\d+$/', $levelid)) { 221 // insert level into DB 222 $data = array('criterionid' => $id, 'definitionformat' => FORMAT_MOODLE); // TODO MDL-31235 format is not supported yet 223 foreach ($levelfields as $key) { 224 if (array_key_exists($key, $level)) { 225 $data[$key] = $level[$key]; 226 } 227 } 228 if ($doupdate) { 229 $levelid = $DB->insert_record('gradingform_rubric_levels', $data); 230 } 231 if ($criterionmaxscore !== null && $criterionmaxscore >= $level['score']) { 232 // new level is added but the maximum score for this criteria did not change, re-grading may not be necessary 233 $haschanges[2] = true; 234 } else { 235 $haschanges[3] = true; 236 } 237 } else { 238 // update level in DB 239 $data = array(); 240 foreach ($levelfields as $key) { 241 if (array_key_exists($key, $level) && $level[$key] != $currentcriteria[$id]['levels'][$levelid][$key]) { 242 $data[$key] = $level[$key]; 243 } 244 } 245 if (!empty($data)) { 246 // update only if something is changed 247 $data['id'] = $levelid; 248 if ($doupdate) { 249 $DB->update_record('gradingform_rubric_levels', $data); 250 } 251 if (isset($data['score'])) { 252 $haschanges[3] = true; 253 } 254 $haschanges[1] = true; 255 } 256 } 257 } 258 } 259 // remove deleted criteria from DB 260 foreach (array_keys($currentcriteria) as $id) { 261 if (!array_key_exists($id, $newcriteria)) { 262 if ($doupdate) { 263 $DB->delete_records('gradingform_rubric_criteria', array('id' => $id)); 264 $DB->delete_records('gradingform_rubric_levels', array('criterionid' => $id)); 265 } 266 $haschanges[3] = true; 267 } 268 } 269 foreach (array('status', 'description', 'descriptionformat', 'name', 'options') as $key) { 270 if (isset($newdefinition->$key) && $newdefinition->$key != $this->definition->$key) { 271 $haschanges[1] = true; 272 } 273 } 274 if ($usermodified && $usermodified != $this->definition->usermodified) { 275 $haschanges[1] = true; 276 } 277 if (!count($haschanges)) { 278 return 0; 279 } 280 if ($doupdate) { 281 parent::update_definition($newdefinition, $usermodified); 282 $this->load_definition(); 283 } 284 // return the maximum level of changes 285 $changelevels = array_keys($haschanges); 286 sort($changelevels); 287 return array_pop($changelevels); 288 } 289 290 /** 291 * Marks all instances filled with this rubric with the status INSTANCE_STATUS_NEEDUPDATE 292 */ 293 public function mark_for_regrade() { 294 global $DB; 295 if ($this->has_active_instances()) { 296 $conditions = array('definitionid' => $this->definition->id, 297 'status' => gradingform_instance::INSTANCE_STATUS_ACTIVE); 298 $DB->set_field('grading_instances', 'status', gradingform_instance::INSTANCE_STATUS_NEEDUPDATE, $conditions); 299 } 300 } 301 302 /** 303 * Loads the rubric form definition if it exists 304 * 305 * There is a new array called 'rubric_criteria' appended to the list of parent's definition properties. 306 */ 307 protected function load_definition() { 308 global $DB; 309 $sql = "SELECT gd.*, 310 rc.id AS rcid, rc.sortorder AS rcsortorder, rc.description AS rcdescription, rc.descriptionformat AS rcdescriptionformat, 311 rl.id AS rlid, rl.score AS rlscore, rl.definition AS rldefinition, rl.definitionformat AS rldefinitionformat 312 FROM {grading_definitions} gd 313 LEFT JOIN {gradingform_rubric_criteria} rc ON (rc.definitionid = gd.id) 314 LEFT JOIN {gradingform_rubric_levels} rl ON (rl.criterionid = rc.id) 315 WHERE gd.areaid = :areaid AND gd.method = :method 316 ORDER BY rc.sortorder,rl.score"; 317 $params = array('areaid' => $this->areaid, 'method' => $this->get_method_name()); 318 319 $rs = $DB->get_recordset_sql($sql, $params); 320 $this->definition = false; 321 foreach ($rs as $record) { 322 // pick the common definition data 323 if ($this->definition === false) { 324 $this->definition = new stdClass(); 325 foreach (array('id', 'name', 'description', 'descriptionformat', 'status', 'copiedfromid', 326 'timecreated', 'usercreated', 'timemodified', 'usermodified', 'timecopied', 'options') as $fieldname) { 327 $this->definition->$fieldname = $record->$fieldname; 328 } 329 $this->definition->rubric_criteria = array(); 330 } 331 // pick the criterion data 332 if (!empty($record->rcid) and empty($this->definition->rubric_criteria[$record->rcid])) { 333 foreach (array('id', 'sortorder', 'description', 'descriptionformat') as $fieldname) { 334 $this->definition->rubric_criteria[$record->rcid][$fieldname] = $record->{'rc'.$fieldname}; 335 } 336 $this->definition->rubric_criteria[$record->rcid]['levels'] = array(); 337 } 338 // pick the level data 339 if (!empty($record->rlid)) { 340 foreach (array('id', 'score', 'definition', 'definitionformat') as $fieldname) { 341 $value = $record->{'rl'.$fieldname}; 342 if ($fieldname == 'score') { 343 $value = (float)$value; // To prevent display like 1.00000 344 } 345 $this->definition->rubric_criteria[$record->rcid]['levels'][$record->rlid][$fieldname] = $value; 346 } 347 } 348 } 349 $rs->close(); 350 $options = $this->get_options(); 351 if (!$options['sortlevelsasc']) { 352 foreach (array_keys($this->definition->rubric_criteria) as $rcid) { 353 $this->definition->rubric_criteria[$rcid]['levels'] = array_reverse($this->definition->rubric_criteria[$rcid]['levels'], true); 354 } 355 } 356 } 357 358 /** 359 * Returns the default options for the rubric display 360 * 361 * @return array 362 */ 363 public static function get_default_options() { 364 $options = array( 365 'sortlevelsasc' => 1, 366 'lockzeropoints' => 1, 367 'alwaysshowdefinition' => 1, 368 'showdescriptionteacher' => 1, 369 'showdescriptionstudent' => 1, 370 'showscoreteacher' => 1, 371 'showscorestudent' => 1, 372 'enableremarks' => 1, 373 'showremarksstudent' => 1 374 ); 375 return $options; 376 } 377 378 /** 379 * Gets the options of this rubric definition, fills the missing options with default values 380 * 381 * The only exception is 'lockzeropoints' - if other options are present in the json string but this 382 * one is absent, this means that the rubric was created before Moodle 3.2 and the 0 value should be used. 383 * 384 * @return array 385 */ 386 public function get_options() { 387 $options = self::get_default_options(); 388 if (!empty($this->definition->options)) { 389 $thisoptions = json_decode($this->definition->options, true); // Assoc. array is expected. 390 foreach ($thisoptions as $option => $value) { 391 $options[$option] = $value; 392 } 393 if (!array_key_exists('lockzeropoints', $thisoptions)) { 394 // Rubrics created before Moodle 3.2 don't have 'lockzeropoints' option. In this case they should not 395 // assume default value 1 but use "legacy" value 0. 396 $options['lockzeropoints'] = 0; 397 } 398 } 399 return $options; 400 } 401 402 /** 403 * Converts the current definition into an object suitable for the editor form's set_data() 404 * 405 * @param boolean $addemptycriterion whether to add an empty criterion if the rubric is completely empty (just being created) 406 * @return stdClass 407 */ 408 public function get_definition_for_editing($addemptycriterion = false) { 409 410 $definition = $this->get_definition(); 411 $properties = new stdClass(); 412 $properties->areaid = $this->areaid; 413 if ($definition) { 414 foreach (array('id', 'name', 'description', 'descriptionformat', 'status') as $key) { 415 $properties->$key = $definition->$key; 416 } 417 $options = self::description_form_field_options($this->get_context()); 418 $properties = file_prepare_standard_editor($properties, 'description', $options, $this->get_context(), 419 'grading', 'description', $definition->id); 420 } 421 $properties->rubric = array('criteria' => array(), 'options' => $this->get_options()); 422 if (!empty($definition->rubric_criteria)) { 423 $properties->rubric['criteria'] = $definition->rubric_criteria; 424 } else if (!$definition && $addemptycriterion) { 425 $properties->rubric['criteria'] = array('addcriterion' => 1); 426 } 427 428 return $properties; 429 } 430 431 /** 432 * Returns the form definition suitable for cloning into another area 433 * 434 * @see parent::get_definition_copy() 435 * @param gradingform_controller $target the controller of the new copy 436 * @return stdClass definition structure to pass to the target's {@link update_definition()} 437 */ 438 public function get_definition_copy(gradingform_controller $target) { 439 440 $new = parent::get_definition_copy($target); 441 $old = $this->get_definition_for_editing(); 442 $new->description_editor = $old->description_editor; 443 $new->rubric = array('criteria' => array(), 'options' => $old->rubric['options']); 444 $newcritid = 1; 445 $newlevid = 1; 446 foreach ($old->rubric['criteria'] as $oldcritid => $oldcrit) { 447 unset($oldcrit['id']); 448 if (isset($oldcrit['levels'])) { 449 foreach ($oldcrit['levels'] as $oldlevid => $oldlev) { 450 unset($oldlev['id']); 451 $oldcrit['levels']['NEWID'.$newlevid] = $oldlev; 452 unset($oldcrit['levels'][$oldlevid]); 453 $newlevid++; 454 } 455 } else { 456 $oldcrit['levels'] = array(); 457 } 458 $new->rubric['criteria']['NEWID'.$newcritid] = $oldcrit; 459 $newcritid++; 460 } 461 462 return $new; 463 } 464 465 /** 466 * Options for displaying the rubric description field in the form 467 * 468 * @param object $context 469 * @return array options for the form description field 470 */ 471 public static function description_form_field_options($context) { 472 global $CFG; 473 return array( 474 'maxfiles' => -1, 475 'maxbytes' => get_user_max_upload_file_size($context, $CFG->maxbytes), 476 'context' => $context, 477 ); 478 } 479 480 /** 481 * Formats the definition description for display on page 482 * 483 * @return string 484 */ 485 public function get_formatted_description() { 486 if (!isset($this->definition->description)) { 487 return ''; 488 } 489 $context = $this->get_context(); 490 491 $options = self::description_form_field_options($this->get_context()); 492 $description = file_rewrite_pluginfile_urls($this->definition->description, 'pluginfile.php', $context->id, 493 'grading', 'description', $this->definition->id, $options); 494 495 $formatoptions = array( 496 'noclean' => false, 497 'trusted' => false, 498 'filter' => true, 499 'context' => $context 500 ); 501 return format_text($description, $this->definition->descriptionformat, $formatoptions); 502 } 503 504 /** 505 * Returns the rubric plugin renderer 506 * 507 * @param moodle_page $page the target page 508 * @return gradingform_rubric_renderer 509 */ 510 public function get_renderer(moodle_page $page) { 511 return $page->get_renderer('gradingform_'. $this->get_method_name()); 512 } 513 514 /** 515 * Returns the HTML code displaying the preview of the grading form 516 * 517 * @param moodle_page $page the target page 518 * @return string 519 */ 520 public function render_preview(moodle_page $page) { 521 522 if (!$this->is_form_defined()) { 523 throw new coding_exception('It is the caller\'s responsibility to make sure that the form is actually defined'); 524 } 525 526 $criteria = $this->definition->rubric_criteria; 527 $options = $this->get_options(); 528 $rubric = ''; 529 if (has_capability('moodle/grade:managegradingforms', $page->context)) { 530 $showdescription = true; 531 } else { 532 if (empty($options['alwaysshowdefinition'])) { 533 // ensure we don't display unless show rubric option enabled 534 return ''; 535 } 536 $showdescription = $options['showdescriptionstudent']; 537 } 538 $output = $this->get_renderer($page); 539 if ($showdescription) { 540 $rubric .= $output->box($this->get_formatted_description(), 'gradingform_rubric-description'); 541 } 542 if (has_capability('moodle/grade:managegradingforms', $page->context)) { 543 if (!$options['lockzeropoints']) { 544 // Warn about using grade calculation method where minimum number of points is flexible. 545 $rubric .= $output->display_rubric_mapping_explained($this->get_min_max_score()); 546 } 547 $rubric .= $output->display_rubric($criteria, $options, self::DISPLAY_PREVIEW, 'rubric'); 548 } else { 549 $rubric .= $output->display_rubric($criteria, $options, self::DISPLAY_PREVIEW_GRADED, 'rubric'); 550 } 551 552 return $rubric; 553 } 554 555 /** 556 * Deletes the rubric definition and all the associated information 557 */ 558 protected function delete_plugin_definition() { 559 global $DB; 560 561 // get the list of instances 562 $instances = array_keys($DB->get_records('grading_instances', array('definitionid' => $this->definition->id), '', 'id')); 563 // delete all fillings 564 $DB->delete_records_list('gradingform_rubric_fillings', 'instanceid', $instances); 565 // delete instances 566 $DB->delete_records_list('grading_instances', 'id', $instances); 567 // get the list of criteria records 568 $criteria = array_keys($DB->get_records('gradingform_rubric_criteria', array('definitionid' => $this->definition->id), '', 'id')); 569 // delete levels 570 $DB->delete_records_list('gradingform_rubric_levels', 'criterionid', $criteria); 571 // delete critera 572 $DB->delete_records_list('gradingform_rubric_criteria', 'id', $criteria); 573 } 574 575 /** 576 * If instanceid is specified and grading instance exists and it is created by this rater for 577 * this item, this instance is returned. 578 * If there exists a draft for this raterid+itemid, take this draft (this is the change from parent) 579 * Otherwise new instance is created for the specified rater and itemid 580 * 581 * @param int $instanceid 582 * @param int $raterid 583 * @param int $itemid 584 * @return gradingform_instance 585 */ 586 public function get_or_create_instance($instanceid, $raterid, $itemid) { 587 global $DB; 588 if ($instanceid && 589 $instance = $DB->get_record('grading_instances', array('id' => $instanceid, 'raterid' => $raterid, 'itemid' => $itemid), '*', IGNORE_MISSING)) { 590 return $this->get_instance($instance); 591 } 592 if ($itemid && $raterid) { 593 $params = array('definitionid' => $this->definition->id, 'raterid' => $raterid, 'itemid' => $itemid); 594 if ($rs = $DB->get_records('grading_instances', $params, 'timemodified DESC', '*', 0, 1)) { 595 $record = reset($rs); 596 $currentinstance = $this->get_current_instance($raterid, $itemid); 597 if ($record->status == gradingform_rubric_instance::INSTANCE_STATUS_INCOMPLETE && 598 (!$currentinstance || $record->timemodified > $currentinstance->get_data('timemodified'))) { 599 $record->isrestored = true; 600 return $this->get_instance($record); 601 } 602 } 603 } 604 return $this->create_instance($raterid, $itemid); 605 } 606 607 /** 608 * Returns html code to be included in student's feedback. 609 * 610 * @param moodle_page $page 611 * @param int $itemid 612 * @param array $gradinginfo result of function grade_get_grades 613 * @param string $defaultcontent default string to be returned if no active grading is found 614 * @param boolean $cangrade whether current user has capability to grade in this context 615 * @return string 616 */ 617 public function render_grade($page, $itemid, $gradinginfo, $defaultcontent, $cangrade) { 618 return $this->get_renderer($page)->display_instances($this->get_active_instances($itemid), $defaultcontent, $cangrade); 619 } 620 621 // ///// full-text search support ///////////////////////////////////////////// 622 623 /** 624 * Prepare the part of the search query to append to the FROM statement 625 * 626 * @param string $gdid the alias of grading_definitions.id column used by the caller 627 * @return string 628 */ 629 public static function sql_search_from_tables($gdid) { 630 return " LEFT JOIN {gradingform_rubric_criteria} rc ON (rc.definitionid = $gdid) 631 LEFT JOIN {gradingform_rubric_levels} rl ON (rl.criterionid = rc.id)"; 632 } 633 634 /** 635 * Prepare the parts of the SQL WHERE statement to search for the given token 636 * 637 * The returned array cosists of the list of SQL comparions and the list of 638 * respective parameters for the comparisons. The returned chunks will be joined 639 * with other conditions using the OR operator. 640 * 641 * @param string $token token to search for 642 * @return array 643 */ 644 public static function sql_search_where($token) { 645 global $DB; 646 647 $subsql = array(); 648 $params = array(); 649 650 // search in rubric criteria description 651 $subsql[] = $DB->sql_like('rc.description', '?', false, false); 652 $params[] = '%'.$DB->sql_like_escape($token).'%'; 653 654 // search in rubric levels definition 655 $subsql[] = $DB->sql_like('rl.definition', '?', false, false); 656 $params[] = '%'.$DB->sql_like_escape($token).'%'; 657 658 return array($subsql, $params); 659 } 660 661 /** 662 * Calculates and returns the possible minimum and maximum score (in points) for this rubric 663 * 664 * @return array 665 */ 666 public function get_min_max_score() { 667 if (!$this->is_form_available()) { 668 return null; 669 } 670 $returnvalue = array('minscore' => 0, 'maxscore' => 0); 671 foreach ($this->get_definition()->rubric_criteria as $id => $criterion) { 672 $scores = array(); 673 foreach ($criterion['levels'] as $level) { 674 $scores[] = $level['score']; 675 } 676 sort($scores); 677 $returnvalue['minscore'] += $scores[0]; 678 $returnvalue['maxscore'] += $scores[sizeof($scores)-1]; 679 } 680 return $returnvalue; 681 } 682 683 /** 684 * @return array An array containing a single key/value pair with the 'rubric_criteria' external_multiple_structure. 685 * @see gradingform_controller::get_external_definition_details() 686 * @since Moodle 2.5 687 */ 688 public static function get_external_definition_details() { 689 $rubric_criteria = new external_multiple_structure( 690 new external_single_structure( 691 array( 692 'id' => new external_value(PARAM_INT, 'criterion id', VALUE_OPTIONAL), 693 'sortorder' => new external_value(PARAM_INT, 'sortorder', VALUE_OPTIONAL), 694 'description' => new external_value(PARAM_RAW, 'description', VALUE_OPTIONAL), 695 'descriptionformat' => new external_format_value('description', VALUE_OPTIONAL), 696 'levels' => new external_multiple_structure( 697 new external_single_structure( 698 array( 699 'id' => new external_value(PARAM_INT, 'level id', VALUE_OPTIONAL), 700 'score' => new external_value(PARAM_FLOAT, 'score', VALUE_OPTIONAL), 701 'definition' => new external_value(PARAM_RAW, 'definition', VALUE_OPTIONAL), 702 'definitionformat' => new external_format_value('definition', VALUE_OPTIONAL) 703 ) 704 ), 'levels', VALUE_OPTIONAL 705 ) 706 ) 707 ), 'definition details', VALUE_OPTIONAL 708 ); 709 return array('rubric_criteria' => $rubric_criteria); 710 } 711 712 /** 713 * Returns an array that defines the structure of the rubric's filling. This function is used by 714 * the web service function core_grading_external::get_gradingform_instances(). 715 * 716 * @return An array containing a single key/value pair with the 'criteria' external_multiple_structure 717 * @see gradingform_controller::get_external_instance_filling_details() 718 * @since Moodle 2.6 719 */ 720 public static function get_external_instance_filling_details() { 721 $criteria = new external_multiple_structure( 722 new external_single_structure( 723 array( 724 'id' => new external_value(PARAM_INT, 'filling id'), 725 'criterionid' => new external_value(PARAM_INT, 'criterion id'), 726 'levelid' => new external_value(PARAM_INT, 'level id', VALUE_OPTIONAL), 727 'remark' => new external_value(PARAM_RAW, 'remark', VALUE_OPTIONAL), 728 'remarkformat' => new external_format_value('remark', VALUE_OPTIONAL) 729 ) 730 ), 'filling', VALUE_OPTIONAL 731 ); 732 return array ('criteria' => $criteria); 733 } 734 735 } 736 737 /** 738 * Class to manage one rubric grading instance. 739 * 740 * Stores information and performs actions like update, copy, validate, submit, etc. 741 * 742 * @package gradingform_rubric 743 * @copyright 2011 Marina Glancy 744 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 745 */ 746 class gradingform_rubric_instance extends gradingform_instance { 747 748 /** @var array stores the rubric, has two keys: 'criteria' and 'options' */ 749 protected $rubric; 750 751 /** 752 * Deletes this (INCOMPLETE) instance from database. 753 */ 754 public function cancel() { 755 global $DB; 756 parent::cancel(); 757 $DB->delete_records('gradingform_rubric_fillings', array('instanceid' => $this->get_id())); 758 } 759 760 /** 761 * Duplicates the instance before editing (optionally substitutes raterid and/or itemid with 762 * the specified values) 763 * 764 * @param int $raterid value for raterid in the duplicate 765 * @param int $itemid value for itemid in the duplicate 766 * @return int id of the new instance 767 */ 768 public function copy($raterid, $itemid) { 769 global $DB; 770 $instanceid = parent::copy($raterid, $itemid); 771 $currentgrade = $this->get_rubric_filling(); 772 foreach ($currentgrade['criteria'] as $criterionid => $record) { 773 $params = array('instanceid' => $instanceid, 'criterionid' => $criterionid, 774 'levelid' => $record['levelid'], 'remark' => $record['remark'], 'remarkformat' => $record['remarkformat']); 775 $DB->insert_record('gradingform_rubric_fillings', $params); 776 } 777 return $instanceid; 778 } 779 780 /** 781 * Determines whether the submitted form was empty. 782 * 783 * @param array $elementvalue value of element submitted from the form 784 * @return boolean true if the form is empty 785 */ 786 public function is_empty_form($elementvalue) { 787 $criteria = $this->get_controller()->get_definition()->rubric_criteria; 788 789 foreach ($criteria as $id => $criterion) { 790 if (isset($elementvalue['criteria'][$id]['levelid']) 791 || !empty($elementvalue['criteria'][$id]['remark'])) { 792 return false; 793 } 794 } 795 return true; 796 } 797 798 /** 799 * Removes the attempt from the gradingform_guide_fillings table 800 * @param array $data the attempt data 801 */ 802 public function clear_attempt($data) { 803 global $DB; 804 805 foreach ($data['criteria'] as $criterionid => $record) { 806 $DB->delete_records('gradingform_rubric_fillings', 807 array('criterionid' => $criterionid, 'instanceid' => $this->get_id())); 808 } 809 } 810 811 /** 812 * Validates that rubric is fully completed and contains valid grade on each criterion 813 * 814 * @param array $elementvalue value of element as came in form submit 815 * @return boolean true if the form data is validated and contains no errors 816 */ 817 public function validate_grading_element($elementvalue) { 818 $criteria = $this->get_controller()->get_definition()->rubric_criteria; 819 if (!isset($elementvalue['criteria']) || !is_array($elementvalue['criteria']) || sizeof($elementvalue['criteria']) < sizeof($criteria)) { 820 return false; 821 } 822 foreach ($criteria as $id => $criterion) { 823 if (!isset($elementvalue['criteria'][$id]['levelid']) 824 || !array_key_exists($elementvalue['criteria'][$id]['levelid'], $criterion['levels'])) { 825 return false; 826 } 827 } 828 return true; 829 } 830 831 /** 832 * Retrieves from DB and returns the data how this rubric was filled 833 * 834 * @param boolean $force whether to force DB query even if the data is cached 835 * @return array 836 */ 837 public function get_rubric_filling($force = false) { 838 global $DB; 839 if ($this->rubric === null || $force) { 840 $records = $DB->get_records('gradingform_rubric_fillings', array('instanceid' => $this->get_id())); 841 $this->rubric = array('criteria' => array()); 842 foreach ($records as $record) { 843 $this->rubric['criteria'][$record->criterionid] = (array)$record; 844 } 845 } 846 return $this->rubric; 847 } 848 849 /** 850 * Updates the instance with the data received from grading form. This function may be 851 * called via AJAX when grading is not yet completed, so it does not change the 852 * status of the instance. 853 * 854 * @param array $data 855 */ 856 public function update($data) { 857 global $DB; 858 $currentgrade = $this->get_rubric_filling(); 859 parent::update($data); 860 foreach ($data['criteria'] as $criterionid => $record) { 861 if (!array_key_exists($criterionid, $currentgrade['criteria'])) { 862 $newrecord = array('instanceid' => $this->get_id(), 'criterionid' => $criterionid, 863 'levelid' => $record['levelid'], 'remarkformat' => FORMAT_MOODLE); 864 if (isset($record['remark'])) { 865 $newrecord['remark'] = $record['remark']; 866 } 867 $DB->insert_record('gradingform_rubric_fillings', $newrecord); 868 } else { 869 $newrecord = array('id' => $currentgrade['criteria'][$criterionid]['id']); 870 foreach (array('levelid', 'remark'/*, 'remarkformat' */) as $key) { 871 // TODO MDL-31235 format is not supported yet 872 if (isset($record[$key]) && $currentgrade['criteria'][$criterionid][$key] != $record[$key]) { 873 $newrecord[$key] = $record[$key]; 874 } 875 } 876 if (count($newrecord) > 1) { 877 $DB->update_record('gradingform_rubric_fillings', $newrecord); 878 } 879 } 880 } 881 foreach ($currentgrade['criteria'] as $criterionid => $record) { 882 if (!array_key_exists($criterionid, $data['criteria'])) { 883 $DB->delete_records('gradingform_rubric_fillings', array('id' => $record['id'])); 884 } 885 } 886 $this->get_rubric_filling(true); 887 } 888 889 /** 890 * Calculates the grade to be pushed to the gradebook 891 * 892 * @return float|int the valid grade from $this->get_controller()->get_grade_range() 893 */ 894 public function get_grade() { 895 $grade = $this->get_rubric_filling(); 896 897 if (!($scores = $this->get_controller()->get_min_max_score()) || $scores['maxscore'] <= $scores['minscore']) { 898 return -1; 899 } 900 901 $graderange = array_keys($this->get_controller()->get_grade_range()); 902 if (empty($graderange)) { 903 return -1; 904 } 905 sort($graderange); 906 $mingrade = $graderange[0]; 907 $maxgrade = $graderange[sizeof($graderange) - 1]; 908 909 $curscore = 0; 910 foreach ($grade['criteria'] as $id => $record) { 911 $curscore += $this->get_controller()->get_definition()->rubric_criteria[$id]['levels'][$record['levelid']]['score']; 912 } 913 914 $allowdecimals = $this->get_controller()->get_allow_grade_decimals(); 915 $options = $this->get_controller()->get_options(); 916 917 if ($options['lockzeropoints']) { 918 // Grade calculation method when 0-level is locked. 919 $grade = max($mingrade, $curscore / $scores['maxscore'] * $maxgrade); 920 return $allowdecimals ? $grade : round($grade, 0); 921 } else { 922 // Alternative grade calculation method. 923 $gradeoffset = ($curscore - $scores['minscore']) / ($scores['maxscore'] - $scores['minscore']) * ($maxgrade - $mingrade); 924 return ($allowdecimals ? $gradeoffset : round($gradeoffset, 0)) + $mingrade; 925 } 926 } 927 928 /** 929 * Returns html for form element of type 'grading'. 930 * 931 * @param moodle_page $page 932 * @param MoodleQuickForm_grading $gradingformelement 933 * @return string 934 */ 935 public function render_grading_element($page, $gradingformelement) { 936 global $USER; 937 if (!$gradingformelement->_flagFrozen) { 938 $module = array('name'=>'gradingform_rubric', 'fullpath'=>'/grade/grading/form/rubric/js/rubric.js'); 939 $page->requires->js_init_call('M.gradingform_rubric.init', array(array('name' => $gradingformelement->getName())), true, $module); 940 $mode = gradingform_rubric_controller::DISPLAY_EVAL; 941 } else { 942 if ($gradingformelement->_persistantFreeze) { 943 $mode = gradingform_rubric_controller::DISPLAY_EVAL_FROZEN; 944 } else { 945 $mode = gradingform_rubric_controller::DISPLAY_REVIEW; 946 } 947 } 948 $criteria = $this->get_controller()->get_definition()->rubric_criteria; 949 $options = $this->get_controller()->get_options(); 950 $value = $gradingformelement->getValue(); 951 $html = ''; 952 if ($value === null) { 953 $value = $this->get_rubric_filling(); 954 } else if (!$this->validate_grading_element($value)) { 955 $html .= html_writer::tag('div', get_string('rubricnotcompleted', 'gradingform_rubric'), array('class' => 'gradingform_rubric-error')); 956 } 957 $currentinstance = $this->get_current_instance(); 958 if ($currentinstance && $currentinstance->get_status() == gradingform_instance::INSTANCE_STATUS_NEEDUPDATE) { 959 $html .= html_writer::div(get_string('needregrademessage', 'gradingform_rubric'), 'gradingform_rubric-regrade', 960 array('role' => 'alert')); 961 } 962 $haschanges = false; 963 if ($currentinstance) { 964 $curfilling = $currentinstance->get_rubric_filling(); 965 foreach ($curfilling['criteria'] as $criterionid => $curvalues) { 966 $value['criteria'][$criterionid]['savedlevelid'] = $curvalues['levelid']; 967 $newremark = null; 968 $newlevelid = null; 969 if (isset($value['criteria'][$criterionid]['remark'])) $newremark = $value['criteria'][$criterionid]['remark']; 970 if (isset($value['criteria'][$criterionid]['levelid'])) $newlevelid = $value['criteria'][$criterionid]['levelid']; 971 if ($newlevelid != $curvalues['levelid'] || $newremark != $curvalues['remark']) { 972 $haschanges = true; 973 } 974 } 975 } 976 if ($this->get_data('isrestored') && $haschanges) { 977 $html .= html_writer::tag('div', get_string('restoredfromdraft', 'gradingform_rubric'), array('class' => 'gradingform_rubric-restored')); 978 } 979 if (!empty($options['showdescriptionteacher'])) { 980 $html .= html_writer::tag('div', $this->get_controller()->get_formatted_description(), array('class' => 'gradingform_rubric-description')); 981 } 982 $html .= $this->get_controller()->get_renderer($page)->display_rubric($criteria, $options, $mode, $gradingformelement->getName(), $value); 983 return $html; 984 } 985 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body