Differences Between: [Versions 402 and 403]
1 <?php 2 3 // This file is part of Moodle - http://moodle.org/ 4 // 5 // Moodle is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // Moodle is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU General Public License for more details. 14 // 15 // You should have received a copy of the GNU General Public License 16 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 17 18 /** 19 * This file defines a class with rubric grading strategy logic 20 * 21 * @package workshopform_rubric 22 * @copyright 2009 David Mudrak <david.mudrak@gmail.com> 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 */ 25 26 defined('MOODLE_INTERNAL') || die(); 27 28 require_once (__DIR__ . '/../lib.php'); // Interface definition. 29 require_once($CFG->libdir . '/gradelib.php'); // To handle float vs decimal issues. 30 31 /** 32 * Server workshop files 33 * 34 * @category files 35 * @param stdClass $course course object 36 * @param stdClass $cm course module object 37 * @param stdClass $context context object 38 * @param string $filearea file area 39 * @param array $args extra arguments 40 * @param bool $forcedownload whether or not force download 41 * @param array $options additional options affecting the file serving 42 * @return bool 43 */ 44 function workshopform_rubric_pluginfile($course, $cm, $context, $filearea, array $args, $forcedownload, array $options=array()) { 45 global $DB; 46 47 if ($context->contextlevel != CONTEXT_MODULE) { 48 return false; 49 } 50 51 require_login($course, true, $cm); 52 53 if ($filearea !== 'description') { 54 return false; 55 } 56 57 $itemid = (int)array_shift($args); // the id of the assessment form dimension 58 if (!$workshop = $DB->get_record('workshop', array('id' => $cm->instance))) { 59 send_file_not_found(); 60 } 61 62 if (!$dimension = $DB->get_record('workshopform_rubric', array('id' => $itemid ,'workshopid' => $workshop->id))) { 63 send_file_not_found(); 64 } 65 66 // TODO now make sure the user is allowed to see the file 67 // (media embedded into the dimension description) 68 $fs = get_file_storage(); 69 $relativepath = implode('/', $args); 70 $fullpath = "/$context->id/workshopform_rubric/$filearea/$itemid/$relativepath"; 71 if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) { 72 return false; 73 } 74 75 // finally send the file 76 send_stored_file($file, 0, 0, $forcedownload, $options); 77 } 78 79 /** 80 * Rubric grading strategy logic. 81 */ 82 class workshop_rubric_strategy implements workshop_strategy { 83 84 /** @const default number of dimensions to show */ 85 const MINDIMS = 3; 86 87 /** @const number of dimensions to add */ 88 const ADDDIMS = 2; 89 90 /** @var workshop the parent workshop instance */ 91 protected $workshop; 92 93 /** @var array definition of the assessment form fields */ 94 protected $dimensions = null; 95 96 /** @var array options for dimension description fields */ 97 protected $descriptionopts; 98 99 /** @var array options for level definition fields */ 100 protected $definitionopts; 101 102 /** @var object rubric configuration */ 103 protected $config; 104 105 /** 106 * Constructor 107 * 108 * @param workshop $workshop The workshop instance record 109 * @return void 110 */ 111 public function __construct(workshop $workshop) { 112 $this->workshop = $workshop; 113 $this->dimensions = $this->load_fields(); 114 $this->config = $this->load_config(); 115 $this->descriptionopts = array('trusttext' => true, 'subdirs' => false, 'maxfiles' => -1); 116 //one day the definitions may become proper wysiwyg fields - not used yet 117 $this->definitionopts = array('trusttext' => true, 'subdirs' => false, 'maxfiles' => -1); 118 } 119 120 /** 121 * Factory method returning an instance of an assessment form editor class 122 * 123 * @param $actionurl URL of form handler, defaults to auto detect the current url 124 */ 125 public function get_edit_strategy_form($actionurl=null) { 126 global $CFG; // needed because the included files use it 127 128 require_once (__DIR__ . '/edit_form.php'); 129 130 $fields = $this->prepare_form_fields($this->dimensions); 131 $fields->config_layout = $this->config->layout; 132 133 $nodimensions = count($this->dimensions); 134 $norepeatsdefault = max($nodimensions + self::ADDDIMS, self::MINDIMS); 135 $norepeats = optional_param('norepeats', $norepeatsdefault, PARAM_INT); // number of dimensions 136 $adddims = optional_param('adddims', '', PARAM_ALPHA); // shall we add more dimensions? 137 if ($adddims) { 138 $norepeats += self::ADDDIMS; 139 } 140 141 // Append editor context to editor options, giving preference to existing context. 142 $this->descriptionopts = array_merge(array('context' => $this->workshop->context), $this->descriptionopts); 143 144 // prepare the embeded files 145 for ($i = 0; $i < $nodimensions; $i++) { 146 // prepare all editor elements 147 $fields = file_prepare_standard_editor($fields, 'description__idx_'.$i, $this->descriptionopts, 148 $this->workshop->context, 'workshopform_rubric', 'description', $fields->{'dimensionid__idx_'.$i}); 149 } 150 151 $customdata = array(); 152 $customdata['workshop'] = $this->workshop; 153 $customdata['strategy'] = $this; 154 $customdata['norepeats'] = $norepeats; 155 $customdata['descriptionopts'] = $this->descriptionopts; 156 $customdata['current'] = $fields; 157 $attributes = array('class' => 'editstrategyform'); 158 159 return new workshop_edit_rubric_strategy_form($actionurl, $customdata, 'post', '', $attributes); 160 } 161 162 /** 163 * Save the assessment dimensions into database 164 * 165 * Saves data into the main strategy form table. If the record->id is null or zero, 166 * new record is created. If the record->id is not empty, the existing record is updated. Records with 167 * empty 'description' field are removed from database. 168 * The passed data object are the raw data returned by the get_data(). 169 * 170 * @uses $DB 171 * @param stdClass $data Raw data returned by the dimension editor form 172 * @return void 173 */ 174 public function save_edit_strategy_form(stdclass $data) { 175 global $DB; 176 177 $norepeats = $data->norepeats; 178 $layout = $data->config_layout; 179 $data = $this->prepare_database_fields($data); 180 $deletedims = array(); // dimension ids to be deleted 181 $deletelevs = array(); // level ids to be deleted 182 183 if ($DB->record_exists('workshopform_rubric_config', array('workshopid' => $this->workshop->id))) { 184 $DB->set_field('workshopform_rubric_config', 'layout', $layout, array('workshopid' => $this->workshop->id)); 185 } else { 186 $record = new stdclass(); 187 $record->workshopid = $this->workshop->id; 188 $record->layout = $layout; 189 $DB->insert_record('workshopform_rubric_config', $record, false); 190 } 191 192 foreach ($data as $record) { 193 if (0 == strlen(trim($record->description_editor['text']))) { 194 if (!empty($record->id)) { 195 // existing record with empty description - to be deleted 196 $deletedims[] = $record->id; 197 foreach ($record->levels as $level) { 198 if (!empty($level->id)) { 199 $deletelevs[] = $level->id; 200 } 201 } 202 } 203 continue; 204 } 205 if (empty($record->id)) { 206 // new field 207 $record->id = $DB->insert_record('workshopform_rubric', $record); 208 } else { 209 // exiting field 210 $DB->update_record('workshopform_rubric', $record); 211 } 212 // re-save with correct path to embeded media files 213 $record = file_postupdate_standard_editor($record, 'description', $this->descriptionopts, 214 $this->workshop->context, 'workshopform_rubric', 'description', $record->id); 215 $DB->update_record('workshopform_rubric', $record); 216 217 // create/update the criterion levels 218 foreach ($record->levels as $level) { 219 $level->dimensionid = $record->id; 220 if (0 == strlen(trim($level->definition))) { 221 if (!empty($level->id)) { 222 $deletelevs[] = $level->id; 223 } 224 continue; 225 } 226 if (empty($level->id)) { 227 // new field 228 $level->id = $DB->insert_record('workshopform_rubric_levels', $level); 229 } else { 230 // exiting field 231 $DB->update_record('workshopform_rubric_levels', $level); 232 } 233 } 234 } 235 $DB->delete_records_list('workshopform_rubric_levels', 'id', $deletelevs); 236 $this->delete_dimensions($deletedims); 237 } 238 239 /** 240 * Factory method returning an instance of an assessment form 241 * 242 * @param moodle_url $actionurl URL of form handler, defaults to auto detect the current url 243 * @param string $mode Mode to open the form in: preview/assessment/readonly 244 * @param stdClass $assessment The current assessment 245 * @param bool $editable 246 * @param array $options 247 */ 248 public function get_assessment_form(moodle_url $actionurl=null, $mode='preview', stdclass $assessment=null, $editable=true, $options=array()) { 249 global $CFG; // needed because the included files use it 250 global $DB; 251 require_once (__DIR__ . '/assessment_form.php'); 252 253 $fields = $this->prepare_form_fields($this->dimensions); 254 $nodimensions = count($this->dimensions); 255 256 // rewrite URLs to the embeded files 257 for ($i = 0; $i < $nodimensions; $i++) { 258 $fields->{'description__idx_'.$i} = file_rewrite_pluginfile_urls($fields->{'description__idx_'.$i}, 259 'pluginfile.php', $this->workshop->context->id, 'workshopform_rubric', 'description', 260 $fields->{'dimensionid__idx_'.$i}); 261 262 } 263 264 if ('assessment' === $mode and !empty($assessment)) { 265 // load the previously saved assessment data 266 $grades = $this->get_current_assessment_data($assessment); 267 $current = new stdclass(); 268 for ($i = 0; $i < $nodimensions; $i++) { 269 $dimid = $fields->{'dimensionid__idx_'.$i}; 270 if (isset($grades[$dimid])) { 271 $givengrade = $grades[$dimid]->grade; 272 // find a level with this grade 273 $levelid = null; 274 foreach ($this->dimensions[$dimid]->levels as $level) { 275 if (grade_floats_equal($level->grade, $givengrade)) { 276 $levelid = $level->id; 277 break; 278 } 279 } 280 $current->{'gradeid__idx_'.$i} = $grades[$dimid]->id; 281 $current->{'chosenlevelid__idx_'.$i} = $levelid; 282 } 283 } 284 } 285 286 // set up the required custom data common for all strategies 287 $customdata['strategy'] = $this; 288 $customdata['workshop'] = $this->workshop; 289 $customdata['mode'] = $mode; 290 $customdata['options'] = $options; 291 292 // set up strategy-specific custom data 293 $customdata['nodims'] = $nodimensions; 294 $customdata['fields'] = $fields; 295 $customdata['current'] = isset($current) ? $current : null; 296 $attributes = array('class' => 'assessmentform rubric ' . $this->config->layout); 297 298 $formclassname = 'workshop_rubric_' . $this->config->layout . '_assessment_form'; 299 return new $formclassname($actionurl, $customdata, 'post', '', $attributes, $editable); 300 } 301 302 /** 303 * Saves the filled assessment 304 * 305 * This method processes data submitted using the form returned by {@link get_assessment_form()} 306 * 307 * @param stdClass $assessment Assessment being filled 308 * @param stdClass $data Raw data as returned by the assessment form 309 * @return float|null Raw grade (0.00000 to 100.00000) for submission as suggested by the peer 310 */ 311 public function save_assessment(stdclass $assessment, stdclass $data) { 312 global $DB; 313 314 for ($i = 0; isset($data->{'dimensionid__idx_' . $i}); $i++) { 315 $grade = new stdclass(); 316 $grade->id = $data->{'gradeid__idx_' . $i}; 317 $grade->assessmentid = $assessment->id; 318 $grade->strategy = 'rubric'; 319 $grade->dimensionid = $data->{'dimensionid__idx_' . $i}; 320 $chosenlevel = $data->{'chosenlevelid__idx_'.$i}; 321 $grade->grade = $this->dimensions[$grade->dimensionid]->levels[$chosenlevel]->grade; 322 323 if (empty($grade->id)) { 324 // new grade 325 $grade->id = $DB->insert_record('workshop_grades', $grade); 326 } else { 327 // updated grade 328 $DB->update_record('workshop_grades', $grade); 329 } 330 } 331 return $this->update_peer_grade($assessment); 332 } 333 334 /** 335 * Has the assessment form been defined and is ready to be used by the reviewers? 336 * 337 * @return boolean 338 */ 339 public function form_ready() { 340 if (count($this->dimensions) > 0) { 341 return true; 342 } 343 return false; 344 } 345 346 /** 347 * @see parent::get_assessments_recordset() 348 */ 349 public function get_assessments_recordset($restrict=null) { 350 global $DB; 351 352 $sql = 'SELECT s.id AS submissionid, 353 a.id AS assessmentid, a.weight AS assessmentweight, a.reviewerid, a.gradinggrade, 354 g.dimensionid, g.grade 355 FROM {workshop_submissions} s 356 JOIN {workshop_assessments} a ON (a.submissionid = s.id) 357 JOIN {workshop_grades} g ON (g.assessmentid = a.id AND g.strategy = :strategy) 358 WHERE s.example=0 AND s.workshopid=:workshopid'; // to be cont. 359 $params = array('workshopid' => $this->workshop->id, 'strategy' => $this->workshop->strategy); 360 361 if (is_null($restrict)) { 362 // update all users - no more conditions 363 } elseif (!empty($restrict)) { 364 list($usql, $uparams) = $DB->get_in_or_equal($restrict, SQL_PARAMS_NAMED); 365 $sql .= " AND a.reviewerid $usql"; 366 $params = array_merge($params, $uparams); 367 } else { 368 throw new coding_exception('Empty value is not a valid parameter here'); 369 } 370 371 $sql .= ' ORDER BY s.id'; // this is important for bulk processing 372 373 return $DB->get_recordset_sql($sql, $params); 374 } 375 376 /** 377 * @see parent::get_dimensions_info() 378 */ 379 public function get_dimensions_info() { 380 global $DB; 381 382 $sql = 'SELECT d.id AS id, MIN(l.grade) AS min, MAX(l.grade) AS max, 1 AS weight 383 FROM {workshopform_rubric} d 384 INNER JOIN {workshopform_rubric_levels} l ON (d.id = l.dimensionid) 385 WHERE d.workshopid = :workshopid 386 GROUP BY d.id'; 387 $params = array('workshopid' => $this->workshop->id); 388 389 return $DB->get_records_sql($sql, $params); 390 } 391 392 /** 393 * Is a given scale used by the instance of workshop? 394 * 395 * This grading strategy does not use scales. 396 * 397 * @param int $scaleid id of the scale to check 398 * @param int|null $workshopid id of workshop instance to check, checks all in case of null 399 * @return bool 400 */ 401 public static function scale_used($scaleid, $workshopid=null) { 402 return false; 403 } 404 405 /** 406 * Delete all data related to a given workshop module instance 407 * 408 * @see workshop_delete_instance() 409 * @param int $workshopid id of the workshop module instance being deleted 410 * @return void 411 */ 412 public static function delete_instance($workshopid) { 413 global $DB; 414 415 $dimensions = $DB->get_records('workshopform_rubric', array('workshopid' => $workshopid), '', 'id'); 416 $DB->delete_records_list('workshopform_rubric_levels', 'dimensionid', array_keys($dimensions)); 417 $DB->delete_records('workshopform_rubric', array('workshopid' => $workshopid)); 418 $DB->delete_records('workshopform_rubric_config', array('workshopid' => $workshopid)); 419 } 420 421 //////////////////////////////////////////////////////////////////////////////// 422 // Internal methods // 423 //////////////////////////////////////////////////////////////////////////////// 424 425 /** 426 * Loads the fields of the assessment form currently used in this workshop 427 * 428 * @return array definition of assessment dimensions 429 */ 430 protected function load_fields() { 431 global $DB; 432 433 $sql = "SELECT r.id AS rid, r.sort, r.description, r.descriptionformat, 434 l.id AS lid, l.grade, l.definition, l.definitionformat 435 FROM {workshopform_rubric} r 436 LEFT JOIN {workshopform_rubric_levels} l ON (l.dimensionid = r.id) 437 WHERE r.workshopid = :workshopid 438 ORDER BY r.sort, l.grade"; 439 $params = array('workshopid' => $this->workshop->id); 440 441 $rs = $DB->get_recordset_sql($sql, $params); 442 $fields = array(); 443 foreach ($rs as $record) { 444 if (!isset($fields[$record->rid])) { 445 $fields[$record->rid] = new stdclass(); 446 $fields[$record->rid]->id = $record->rid; 447 $fields[$record->rid]->sort = $record->sort; 448 $fields[$record->rid]->description = $record->description; 449 $fields[$record->rid]->descriptionformat = $record->descriptionformat; 450 $fields[$record->rid]->levels = array(); 451 } 452 if (!empty($record->lid)) { 453 $fields[$record->rid]->levels[$record->lid] = new stdclass(); 454 $fields[$record->rid]->levels[$record->lid]->id = $record->lid; 455 $fields[$record->rid]->levels[$record->lid]->grade = $record->grade; 456 $fields[$record->rid]->levels[$record->lid]->definition = $record->definition; 457 $fields[$record->rid]->levels[$record->lid]->definitionformat = $record->definitionformat; 458 } 459 } 460 $rs->close(); 461 462 return $fields; 463 } 464 465 /** 466 * Get the configuration for the current rubric strategy 467 * 468 * @return object 469 */ 470 protected function load_config() { 471 global $DB; 472 473 if (!$config = $DB->get_record('workshopform_rubric_config', array('workshopid' => $this->workshop->id), 'layout')) { 474 $config = (object)array('layout' => 'list'); 475 } 476 return $config; 477 } 478 479 /** 480 * Maps the dimension data from DB to the form fields 481 * 482 * @param array $fields Array of dimensions definition as returned by {@link load_fields()} 483 * @return stdclass of fields data to be used by the mform set_data 484 */ 485 protected function prepare_form_fields(array $fields) { 486 487 $formdata = new stdclass(); 488 $key = 0; 489 foreach ($fields as $field) { 490 $formdata->{'dimensionid__idx_' . $key} = $field->id; 491 $formdata->{'description__idx_' . $key} = $field->description; 492 $formdata->{'description__idx_' . $key.'format'} = $field->descriptionformat; 493 $formdata->{'numoflevels__idx_' . $key} = count($field->levels); 494 $lev = 0; 495 foreach ($field->levels as $level) { 496 $formdata->{'levelid__idx_' . $key . '__idy_' . $lev} = $level->id; 497 $formdata->{'grade__idx_' . $key . '__idy_' . $lev} = $level->grade; 498 $formdata->{'definition__idx_' . $key . '__idy_' . $lev} = $level->definition; 499 $formdata->{'definition__idx_' . $key . '__idy_' . $lev . 'format'} = $level->definitionformat; 500 $lev++; 501 } 502 $key++; 503 } 504 return $formdata; 505 } 506 507 /** 508 * Deletes dimensions and removes embedded media from its descriptions 509 * 510 * todo we may check that there are no assessments done using these dimensions and probably remove them 511 * 512 * @param array $masterids 513 * @return void 514 */ 515 protected function delete_dimensions(array $ids) { 516 global $DB; 517 518 $fs = get_file_storage(); 519 foreach ($ids as $id) { 520 if (!empty($id)) { // to prevent accidental removal of all files in the area 521 $fs->delete_area_files($this->workshop->context->id, 'workshopform_rubric', 'description', $id); 522 } 523 } 524 $DB->delete_records_list('workshopform_rubric', 'id', $ids); 525 } 526 527 /** 528 * Prepares data returned by {@link workshop_edit_rubric_strategy_form} so they can be saved into database 529 * 530 * It automatically adds some columns into every record. The sorting is 531 * done by the order of the returned array and starts with 1. 532 * Called internally from {@link save_edit_strategy_form()} only. Could be private but 533 * keeping protected for unit testing purposes. 534 * 535 * @param stdClass $raw Raw data returned by mform 536 * @return array Array of objects to be inserted/updated in DB 537 */ 538 protected function prepare_database_fields(stdclass $raw) { 539 540 $cook = array(); 541 542 for ($i = 0; $i < $raw->norepeats; $i++) { 543 $cook[$i] = new stdclass(); 544 $cook[$i]->id = $raw->{'dimensionid__idx_'.$i}; 545 $cook[$i]->workshopid = $this->workshop->id; 546 $cook[$i]->sort = $i + 1; 547 $cook[$i]->description_editor = $raw->{'description__idx_'.$i.'_editor'}; 548 $cook[$i]->levels = array(); 549 550 $j = 0; 551 while (isset($raw->{'levelid__idx_' . $i . '__idy_' . $j})) { 552 $level = new stdclass(); 553 $level->id = $raw->{'levelid__idx_' . $i . '__idy_' . $j}; 554 $level->grade = $raw->{'grade__idx_'.$i.'__idy_'.$j}; 555 $level->definition = $raw->{'definition__idx_'.$i.'__idy_'.$j}; 556 $level->definitionformat = FORMAT_HTML; 557 $cook[$i]->levels[] = $level; 558 $j++; 559 } 560 } 561 562 return $cook; 563 } 564 565 /** 566 * Returns the list of current grades filled by the reviewer indexed by dimensionid 567 * 568 * @param stdClass $assessment Assessment record 569 * @return array [int dimensionid] => stdclass workshop_grades record 570 */ 571 protected function get_current_assessment_data(stdclass $assessment) { 572 global $DB; 573 574 if (empty($this->dimensions)) { 575 return array(); 576 } 577 list($dimsql, $dimparams) = $DB->get_in_or_equal(array_keys($this->dimensions), SQL_PARAMS_NAMED); 578 // beware! the caller may rely on the returned array is indexed by dimensionid 579 $sql = "SELECT dimensionid, wg.* 580 FROM {workshop_grades} wg 581 WHERE assessmentid = :assessmentid AND strategy= :strategy AND dimensionid $dimsql"; 582 $params = array('assessmentid' => $assessment->id, 'strategy' => 'rubric'); 583 $params = array_merge($params, $dimparams); 584 585 return $DB->get_records_sql($sql, $params); 586 } 587 588 /** 589 * Aggregates the assessment form data and sets the grade for the submission given by the peer 590 * 591 * @param stdClass $assessment Assessment record 592 * @return float|null Raw grade (from 0.00000 to 100.00000) for submission as suggested by the peer 593 */ 594 protected function update_peer_grade(stdclass $assessment) { 595 $grades = $this->get_current_assessment_data($assessment); 596 $suggested = $this->calculate_peer_grade($grades); 597 if (!is_null($suggested)) { 598 $this->workshop->set_peer_grade($assessment->id, $suggested); 599 } 600 return $suggested; 601 } 602 603 /** 604 * Calculates the aggregated grade given by the reviewer 605 * 606 * @param array $grades Grade records as returned by {@link get_current_assessment_data} 607 * @uses $this->dimensions 608 * @return float|null Raw grade (from 0.00000 to 100.00000) for submission as suggested by the peer 609 */ 610 protected function calculate_peer_grade(array $grades) { 611 612 if (empty($grades)) { 613 return null; 614 } 615 616 // summarize the grades given in rubrics 617 $sumgrades = 0; 618 foreach ($grades as $grade) { 619 $sumgrades += $grade->grade; 620 } 621 622 // get the minimal and maximal possible grade (sum of minimal/maximal grades across all dimensions) 623 $mingrade = 0; 624 $maxgrade = 0; 625 foreach ($this->dimensions as $dimension) { 626 $mindimensiongrade = null; 627 $maxdimensiongrade = null; 628 foreach ($dimension->levels as $level) { 629 if (is_null($mindimensiongrade) or $level->grade < $mindimensiongrade) { 630 $mindimensiongrade = $level->grade; 631 } 632 if (is_null($maxdimensiongrade) or $level->grade > $maxdimensiongrade) { 633 $maxdimensiongrade = $level->grade; 634 } 635 } 636 $mingrade += $mindimensiongrade; 637 $maxgrade += $maxdimensiongrade; 638 } 639 640 if ($maxgrade - $mingrade > 0) { 641 return grade_floatval(100 * ($sumgrades - $mingrade) / ($maxgrade - $mingrade)); 642 } else { 643 return null; 644 } 645 } 646 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body