See Release Notes
Long Term Support Release
Differences Between: [Versions 401 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 accumulative grading strategy logic 20 * 21 * @package workshopform_accumulative 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_accumulative_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_accumulative', 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_accumulative/$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 * Accumulative grading strategy logic. 81 */ 82 class workshop_accumulative_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 /** 100 * Constructor 101 * 102 * @param workshop $workshop The workshop instance record 103 * @return void 104 */ 105 public function __construct(workshop $workshop) { 106 $this->workshop = $workshop; 107 $this->dimensions = $this->load_fields(); 108 $this->descriptionopts = array('trusttext' => true, 'subdirs' => false, 'maxfiles' => -1); 109 } 110 111 /** 112 * Factory method returning an instance of an assessment form editor class 113 * 114 * @param $actionurl URL of form handler, defaults to auto detect the current url 115 */ 116 public function get_edit_strategy_form($actionurl=null) { 117 global $CFG; // needed because the included files use it 118 global $PAGE; 119 120 require_once (__DIR__ . '/edit_form.php'); 121 122 $fields = $this->prepare_form_fields($this->dimensions); 123 $nodimensions = count($this->dimensions); 124 $norepeatsdefault = max($nodimensions + self::ADDDIMS, self::MINDIMS); 125 $norepeats = optional_param('norepeats', $norepeatsdefault, PARAM_INT); // number of dimensions 126 $noadddims = optional_param('noadddims', '', PARAM_ALPHA); // shall we add more? 127 if ($noadddims) { 128 $norepeats += self::ADDDIMS; 129 } 130 131 // Append editor context to editor options, giving preference to existing context. 132 $this->descriptionopts = array_merge(array('context' => $PAGE->context), $this->descriptionopts); 133 134 // prepare the embeded files 135 for ($i = 0; $i < $nodimensions; $i++) { 136 // prepare all editor elements 137 $fields = file_prepare_standard_editor($fields, 'description__idx_'.$i, $this->descriptionopts, 138 $PAGE->context, 'workshopform_accumulative', 'description', $fields->{'dimensionid__idx_'.$i}); 139 } 140 141 $customdata = array(); 142 $customdata['workshop'] = $this->workshop; 143 $customdata['strategy'] = $this; 144 $customdata['norepeats'] = $norepeats; 145 $customdata['descriptionopts'] = $this->descriptionopts; 146 $customdata['current'] = $fields; 147 $attributes = array('class' => 'editstrategyform'); 148 149 return new workshop_edit_accumulative_strategy_form($actionurl, $customdata, 'post', '', $attributes); 150 } 151 152 /** 153 * Save the assessment dimensions into database 154 * 155 * Saves data into the main strategy form table. If the record->id is null or zero, 156 * new record is created. If the record->id is not empty, the existing record is updated. Records with 157 * empty 'description' field are removed from database. 158 * The passed data object are the raw data returned by the get_data(). 159 * 160 * @uses $DB 161 * @param stdClass $data Raw data returned by the dimension editor form 162 * @return void 163 */ 164 public function save_edit_strategy_form(stdclass $data) { 165 global $DB, $PAGE; 166 167 $workshopid = $data->workshopid; 168 $norepeats = $data->norepeats; 169 170 $data = $this->prepare_database_fields($data); 171 $records = $data->accumulative; // records to be saved into {workshopform_accumulative} 172 $todelete = array(); // dimension ids to be deleted 173 174 for ($i=0; $i < $norepeats; $i++) { 175 $record = $records[$i]; 176 if (0 == strlen(trim($record->description_editor['text']))) { 177 if (!empty($record->id)) { 178 // existing record with empty description - to be deleted 179 $todelete[] = $record->id; 180 } 181 continue; 182 } 183 if (empty($record->id)) { 184 // new field 185 $record->id = $DB->insert_record('workshopform_accumulative', $record); 186 } else { 187 // exiting field 188 $DB->update_record('workshopform_accumulative', $record); 189 } 190 // re-save with correct path to embeded media files 191 $record = file_postupdate_standard_editor($record, 'description', $this->descriptionopts, 192 $PAGE->context, 'workshopform_accumulative', 'description', $record->id); 193 $DB->update_record('workshopform_accumulative', $record); 194 } 195 $this->delete_dimensions($todelete); 196 } 197 198 /** 199 * Factory method returning an instance of an assessment form 200 * 201 * @param moodle_url $actionurl URL of form handler, defaults to auto detect the current url 202 * @param string $mode Mode to open the form in: preview/assessment 203 * @param stdClass $assessment The current assessment 204 * @param bool $editable 205 * @param array $options 206 */ 207 public function get_assessment_form(moodle_url $actionurl=null, $mode='preview', stdclass $assessment=null, $editable=true, $options=array()) { 208 global $CFG; // needed because the included files use it 209 global $PAGE; 210 global $DB; 211 require_once (__DIR__ . '/assessment_form.php'); 212 213 $fields = $this->prepare_form_fields($this->dimensions); 214 $nodimensions = count($this->dimensions); 215 216 // rewrite URLs to the embeded files 217 for ($i = 0; $i < $nodimensions; $i++) { 218 $fields->{'description__idx_'.$i} = file_rewrite_pluginfile_urls($fields->{'description__idx_'.$i}, 219 'pluginfile.php', $PAGE->context->id, 'workshopform_accumulative', 'description', $fields->{'dimensionid__idx_'.$i}); 220 } 221 222 if ('assessment' === $mode and !empty($assessment)) { 223 // load the previously saved assessment data 224 $grades = $this->get_current_assessment_data($assessment); 225 $current = new stdclass(); 226 for ($i = 0; $i < $nodimensions; $i++) { 227 $dimid = $fields->{'dimensionid__idx_'.$i}; 228 if (isset($grades[$dimid])) { 229 $current->{'gradeid__idx_'.$i} = $grades[$dimid]->id; 230 $current->{'grade__idx_'.$i} = $grades[$dimid]->grade; 231 $current->{'peercomment__idx_'.$i} = $grades[$dimid]->peercomment; 232 } 233 } 234 } 235 236 // set up the required custom data common for all strategies 237 $customdata['strategy'] = $this; 238 $customdata['workshop'] = $this->workshop; 239 $customdata['mode'] = $mode; 240 $customdata['options'] = $options; 241 242 // set up strategy-specific custom data 243 $customdata['nodims'] = $nodimensions; 244 $customdata['fields'] = $fields; 245 $customdata['current'] = isset($current) ? $current : null; 246 $attributes = array('class' => 'assessmentform accumulative'); 247 248 return new workshop_accumulative_assessment_form($actionurl, $customdata, 'post', '', $attributes, $editable); 249 } 250 251 /** 252 * Saves the filled assessment 253 * 254 * This method processes data submitted using the form returned by {@link get_assessment_form()} 255 * 256 * @param stdClass $assessment Assessment being filled 257 * @param stdClass $data Raw data as returned by the assessment form 258 * @return float|null Raw grade (0.00000 to 100.00000) for submission as suggested by the peer 259 */ 260 public function save_assessment(stdclass $assessment, stdclass $data) { 261 global $DB; 262 263 if (!isset($data->nodims)) { 264 throw new coding_exception('You did not send me the number of assessment dimensions to process'); 265 } 266 for ($i = 0; $i < $data->nodims; $i++) { 267 $grade = new stdclass(); 268 $grade->id = $data->{'gradeid__idx_' . $i}; 269 $grade->assessmentid = $assessment->id; 270 $grade->strategy = 'accumulative'; 271 $grade->dimensionid = $data->{'dimensionid__idx_' . $i}; 272 if (isset($data->{'grade__idx_' . $i})) { 273 $grade->grade = $data->{'grade__idx_' . $i}; 274 } 275 $grade->peercomment = $data->{'peercomment__idx_' . $i}; 276 $grade->peercommentformat = FORMAT_MOODLE; 277 if (empty($grade->id)) { 278 // new grade 279 $grade->id = $DB->insert_record('workshop_grades', $grade); 280 } else { 281 // updated grade 282 $DB->update_record('workshop_grades', $grade); 283 } 284 } 285 return $this->update_peer_grade($assessment); 286 } 287 288 /** 289 * Has the assessment form been defined and is ready to be used by the reviewers? 290 * 291 * @return boolean 292 */ 293 public function form_ready() { 294 if (count($this->dimensions) > 0) { 295 return true; 296 } 297 return false; 298 } 299 300 /** 301 * @see parent::get_assessments_recordset() 302 */ 303 public function get_assessments_recordset($restrict=null) { 304 global $DB; 305 306 $sql = 'SELECT s.id AS submissionid, 307 a.id AS assessmentid, a.weight AS assessmentweight, a.reviewerid, a.gradinggrade, 308 g.dimensionid, g.grade 309 FROM {workshop_submissions} s 310 JOIN {workshop_assessments} a ON (a.submissionid = s.id) 311 JOIN {workshop_grades} g ON (g.assessmentid = a.id AND g.strategy = :strategy) 312 WHERE s.example=0 AND s.workshopid=:workshopid'; // to be cont. 313 $params = array('workshopid' => $this->workshop->id, 'strategy' => $this->workshop->strategy); 314 315 if (is_null($restrict)) { 316 // update all users - no more conditions 317 } elseif (!empty($restrict)) { 318 list($usql, $uparams) = $DB->get_in_or_equal($restrict, SQL_PARAMS_NAMED); 319 $sql .= " AND a.reviewerid $usql"; 320 $params = array_merge($params, $uparams); 321 } else { 322 throw new coding_exception('Empty value is not a valid parameter here'); 323 } 324 325 $sql .= ' ORDER BY s.id'; // this is important for bulk processing 326 327 return $DB->get_recordset_sql($sql, $params); 328 } 329 330 /** 331 * @see parent::get_dimensions_info() 332 */ 333 public function get_dimensions_info() { 334 global $DB; 335 336 $sql = 'SELECT d.id, d.grade, d.weight, s.scale 337 FROM {workshopform_accumulative} d 338 LEFT JOIN {scale} s ON (d.grade < 0 AND -d.grade = s.id) 339 WHERE d.workshopid = :workshopid'; 340 $params = array('workshopid' => $this->workshop->id); 341 $dimrecords = $DB->get_records_sql($sql, $params); 342 $diminfo = array(); 343 foreach ($dimrecords as $dimid => $dimrecord) { 344 $diminfo[$dimid] = new stdclass(); 345 $diminfo[$dimid]->id = $dimid; 346 $diminfo[$dimid]->weight = $dimrecord->weight; 347 if ($dimrecord->grade < 0) { 348 // the dimension uses a scale 349 $diminfo[$dimid]->min = 1; 350 $diminfo[$dimid]->max = count(explode(',', $dimrecord->scale)); 351 $diminfo[$dimid]->scale = $dimrecord->scale; 352 } else { 353 // the dimension uses points 354 $diminfo[$dimid]->min = 0; 355 $diminfo[$dimid]->max = grade_floatval($dimrecord->grade); 356 } 357 } 358 return $diminfo; 359 } 360 361 /** 362 * Is a given scale used by the instance of workshop? 363 * 364 * @param int $scaleid id of the scale to check 365 * @param int|null $workshopid id of workshop instance to check, checks all in case of null 366 * @return bool 367 */ 368 public static function scale_used($scaleid, $workshopid=null) { 369 global $DB; 370 371 $conditions['grade'] = -$scaleid; 372 if (!is_null($workshopid)) { 373 $conditions['workshopid'] = $workshopid; 374 } 375 return $DB->record_exists('workshopform_accumulative', $conditions); 376 } 377 378 /** 379 * Delete all data related to a given workshop module instance 380 * 381 * @see workshop_delete_instance() 382 * @param int $workshopid id of the workshop module instance being deleted 383 * @return void 384 */ 385 public static function delete_instance($workshopid) { 386 global $DB; 387 388 $DB->delete_records('workshopform_accumulative', array('workshopid' => $workshopid)); 389 } 390 391 //////////////////////////////////////////////////////////////////////////////// 392 // Internal methods // 393 //////////////////////////////////////////////////////////////////////////////// 394 395 /** 396 * Loads the fields of the assessment form currently used in this workshop 397 * 398 * @return array definition of assessment dimensions 399 */ 400 protected function load_fields() { 401 global $DB; 402 403 $sql = 'SELECT * 404 FROM {workshopform_accumulative} 405 WHERE workshopid = :workshopid 406 ORDER BY sort'; 407 $params = array('workshopid' => $this->workshop->id); 408 409 return $DB->get_records_sql($sql, $params); 410 } 411 412 /** 413 * Maps the dimension data from DB to the form fields 414 * 415 * @param array $raw Array of raw dimension records as returned by {@link load_fields()} 416 * @return array Array of fields data to be used by the mform set_data 417 */ 418 protected function prepare_form_fields(array $raw) { 419 420 $formdata = new stdclass(); 421 $key = 0; 422 foreach ($raw as $dimension) { 423 $formdata->{'dimensionid__idx_' . $key} = $dimension->id; 424 $formdata->{'description__idx_' . $key} = $dimension->description; 425 $formdata->{'description__idx_' . $key.'format'} = $dimension->descriptionformat; 426 $formdata->{'grade__idx_' . $key} = $dimension->grade; 427 $formdata->{'weight__idx_' . $key} = $dimension->weight; 428 $key++; 429 } 430 return $formdata; 431 } 432 433 /** 434 * Deletes dimensions and removes embedded media from its descriptions 435 * 436 * todo we may check that there are no assessments done using these dimensions and probably remove them 437 * 438 * @param array $masterids 439 * @return void 440 */ 441 protected function delete_dimensions(array $ids) { 442 global $DB, $PAGE; 443 444 $fs = get_file_storage(); 445 foreach ($ids as $id) { 446 if (!empty($id)) { // to prevent accidental removal of all files in the area 447 $fs->delete_area_files($PAGE->context->id, 'workshopform_accumulative', 'description', $id); 448 } 449 } 450 $DB->delete_records_list('workshopform_accumulative', 'id', $ids); 451 } 452 453 /** 454 * Prepares data returned by {@link workshop_edit_accumulative_strategy_form} so they can be saved into database 455 * 456 * It automatically adds some columns into every record. The sorting is 457 * done by the order of the returned array and starts with 1. 458 * Called internally from {@link save_edit_strategy_form()} only. Could be private but 459 * keeping protected for unit testing purposes. 460 * 461 * @param stdClass $raw Raw data returned by mform 462 * @return array Array of objects to be inserted/updated in DB 463 */ 464 protected function prepare_database_fields(stdclass $raw) { 465 global $PAGE; 466 467 $cook = new stdclass(); // to be returned 468 $cook->accumulative = array(); // records to be stored in {workshopform_accumulative} 469 470 for ($i = 0; $i < $raw->norepeats; $i++) { 471 $cook->accumulative[$i] = new stdclass(); 472 $cook->accumulative[$i]->id = $raw->{'dimensionid__idx_'.$i}; 473 $cook->accumulative[$i]->workshopid = $this->workshop->id; 474 $cook->accumulative[$i]->sort = $i + 1; 475 $cook->accumulative[$i]->description_editor = $raw->{'description__idx_'.$i.'_editor'}; 476 $cook->accumulative[$i]->grade = $raw->{'grade__idx_'.$i}; 477 $cook->accumulative[$i]->weight = $raw->{'weight__idx_'.$i}; 478 } 479 return $cook; 480 } 481 482 /** 483 * Returns the list of current grades filled by the reviewer indexed by dimensionid 484 * 485 * @param stdClass $assessment Assessment record 486 * @return array [int dimensionid] => stdclass workshop_grades record 487 */ 488 protected function get_current_assessment_data(stdclass $assessment) { 489 global $DB; 490 491 if (empty($this->dimensions)) { 492 return array(); 493 } 494 list($dimsql, $dimparams) = $DB->get_in_or_equal(array_keys($this->dimensions), SQL_PARAMS_NAMED); 495 // beware! the caller may rely on the returned array is indexed by dimensionid 496 $sql = "SELECT dimensionid, wg.* 497 FROM {workshop_grades} wg 498 WHERE assessmentid = :assessmentid AND strategy= :strategy AND dimensionid $dimsql"; 499 $params = array('assessmentid' => $assessment->id, 'strategy' => 'accumulative'); 500 $params = array_merge($params, $dimparams); 501 502 return $DB->get_records_sql($sql, $params); 503 } 504 505 /** 506 * Aggregates the assessment form data and sets the grade for the submission given by the peer 507 * 508 * @param stdClass $assessment Assessment record 509 * @return float|null Raw grade (from 0.00000 to 100.00000) for submission as suggested by the peer 510 */ 511 protected function update_peer_grade(stdclass $assessment) { 512 $grades = $this->get_current_assessment_data($assessment); 513 $suggested = $this->calculate_peer_grade($grades); 514 if (!is_null($suggested)) { 515 $this->workshop->set_peer_grade($assessment->id, $suggested); 516 } 517 return $suggested; 518 } 519 520 /** 521 * Calculates the aggregated grade given by the reviewer 522 * 523 * @param array $grades Grade records as returned by {@link get_current_assessment_data} 524 * @uses $this->dimensions 525 * @return float|null Raw grade (from 0.00000 to 100.00000) for submission as suggested by the peer 526 */ 527 protected function calculate_peer_grade(array $grades) { 528 529 if (empty($grades)) { 530 return null; 531 } 532 $sumgrades = 0; 533 $sumweights = 0; 534 foreach ($grades as $grade) { 535 $dimension = $this->dimensions[$grade->dimensionid]; 536 if ($dimension->weight < 0) { 537 throw new coding_exception('Negative weights are not supported any more. Something is wrong with your data'); 538 } 539 if (grade_floats_equal($dimension->weight, 0) or grade_floats_equal($dimension->grade, 0)) { 540 // does not influence the final grade 541 continue; 542 } 543 if ($dimension->grade < 0) { 544 // this is a scale 545 $scaleid = -$dimension->grade; 546 $sumgrades += $this->scale_to_grade($scaleid, $grade->grade) * $dimension->weight * 100; 547 $sumweights += $dimension->weight; 548 } else { 549 // regular grade 550 $sumgrades += ($grade->grade / $dimension->grade) * $dimension->weight * 100; 551 $sumweights += $dimension->weight; 552 } 553 } 554 555 if ($sumweights === 0) { 556 return 0; 557 } 558 return grade_floatval($sumgrades / $sumweights); 559 } 560 561 /** 562 * Convert scale grade to numerical grades 563 * 564 * In accumulative grading strategy, scales are considered as grades from 0 to M-1, where M is the number of scale items. 565 * 566 * @throws coding_exception 567 * @param string $scaleid Scale identifier 568 * @param int $item Selected scale item number, numbered 1, 2, 3, ... M 569 * @return float 570 */ 571 protected function scale_to_grade($scaleid, $item) { 572 global $DB; 573 574 /** @var cache of numbers of scale items */ 575 static $numofscaleitems = array(); 576 577 if (!isset($numofscaleitems[$scaleid])) { 578 $scale = $DB->get_field('scale', 'scale', array('id' => $scaleid), MUST_EXIST); 579 $items = explode(',', $scale); 580 $numofscaleitems[$scaleid] = count($items); 581 unset($scale); 582 unset($items); 583 } 584 585 if ($numofscaleitems[$scaleid] <= 1) { 586 throw new coding_exception('Invalid scale definition, no scale items found'); 587 } 588 589 if ($item <= 0 or $numofscaleitems[$scaleid] < $item) { 590 throw new coding_exception('Invalid scale item number'); 591 } 592 593 return ($item - 1) / ($numofscaleitems[$scaleid] - 1); 594 } 595 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body