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