|
Moodle
2.2.1
http://www.collinsharper.com
|
00001 <?php 00002 00003 // This file is part of Moodle - http://moodle.org/ 00004 // 00005 // Moodle is free software: you can redistribute it and/or modify 00006 // it under the terms of the GNU General Public License as published by 00007 // the Free Software Foundation, either version 3 of the License, or 00008 // (at your option) any later version. 00009 // 00010 // Moodle is distributed in the hope that it will be useful, 00011 // but WITHOUT ANY WARRANTY; without even the implied warranty of 00012 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 00013 // GNU General Public License for more details. 00014 // 00015 // You should have received a copy of the GNU General Public License 00016 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 00017 00027 defined('MOODLE_INTERNAL') || die(); 00028 00029 require_once(dirname(dirname(__FILE__)) . '/lib.php'); // interface definition 00030 require_once($CFG->libdir . '/gradelib.php'); // to handle float vs decimal issues 00031 00032 function workshopform_numerrors_pluginfile($course, $cm, $context, $filearea, array $args, $forcedownload) { 00033 global $DB; 00034 00035 if ($context->contextlevel != CONTEXT_MODULE) { 00036 return false; 00037 } 00038 00039 require_login($course, true, $cm); 00040 00041 if ($filearea !== 'description') { 00042 return false; 00043 } 00044 00045 $itemid = (int)array_shift($args); // the id of the assessment form dimension 00046 if (!$workshop = $DB->get_record('workshop', array('id' => $cm->instance))) { 00047 send_file_not_found(); 00048 } 00049 00050 if (!$dimension = $DB->get_record('workshopform_numerrors', array('id' => $itemid ,'workshopid' => $workshop->id))) { 00051 send_file_not_found(); 00052 } 00053 00054 // TODO now make sure the user is allowed to see the file 00055 // (media embedded into the dimension description) 00056 $fs = get_file_storage(); 00057 $relativepath = implode('/', $args); 00058 $fullpath = "/$context->id/workshopform_numerrors/$filearea/$itemid/$relativepath"; 00059 if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) { 00060 return false; 00061 } 00062 00063 // finally send the file 00064 send_stored_file($file); 00065 } 00066 00070 class workshop_numerrors_strategy implements workshop_strategy { 00071 00073 const MINDIMS = 3; 00074 00076 const ADDDIMS = 2; 00077 00079 protected $workshop; 00080 00082 protected $dimensions = null; 00083 00085 protected $mappings = null; 00086 00088 protected $descriptionopts; 00089 00096 public function __construct(workshop $workshop) { 00097 $this->workshop = $workshop; 00098 $this->dimensions = $this->load_fields(); 00099 $this->mappings = $this->load_mappings(); 00100 $this->descriptionopts = array('trusttext' => true, 'subdirs' => false, 'maxfiles' => -1); 00101 } 00102 00108 public function get_edit_strategy_form($actionurl=null) { 00109 global $CFG; // needed because the included files use it 00110 global $PAGE; 00111 00112 require_once(dirname(__FILE__) . '/edit_form.php'); 00113 00114 $fields = $this->prepare_form_fields($this->dimensions, $this->mappings); 00115 $nodimensions = count($this->dimensions); 00116 $norepeatsdefault = max($nodimensions + self::ADDDIMS, self::MINDIMS); 00117 $norepeats = optional_param('norepeats', $norepeatsdefault, PARAM_INT); // number of dimensions 00118 $noadddims = optional_param('noadddims', '', PARAM_ALPHA); // shall we add more? 00119 if ($noadddims) { 00120 $norepeats += self::ADDDIMS; 00121 } 00122 00123 // Append editor context to editor options, giving preference to existing context. 00124 $this->descriptionopts = array_merge(array('context' => $PAGE->context), $this->descriptionopts); 00125 00126 // prepare the embeded files 00127 for ($i = 0; $i < $nodimensions; $i++) { 00128 // prepare all editor elements 00129 $fields = file_prepare_standard_editor($fields, 'description__idx_'.$i, $this->descriptionopts, 00130 $PAGE->context, 'workshopform_numerrors', 'description', $fields->{'dimensionid__idx_'.$i}); 00131 } 00132 00133 $customdata = array(); 00134 $customdata['workshop'] = $this->workshop; 00135 $customdata['strategy'] = $this; 00136 $customdata['norepeats'] = $norepeats; 00137 $customdata['nodimensions'] = $nodimensions; 00138 $customdata['descriptionopts'] = $this->descriptionopts; 00139 $customdata['current'] = $fields; 00140 $attributes = array('class' => 'editstrategyform'); 00141 00142 return new workshop_edit_numerrors_strategy_form($actionurl, $customdata, 'post', '', $attributes); 00143 } 00144 00157 public function save_edit_strategy_form(stdclass $data) { 00158 global $DB, $PAGE; 00159 00160 $workshopid = $data->workshopid; 00161 $norepeats = $data->norepeats; 00162 00163 $data = $this->prepare_database_fields($data); 00164 $records = $data->numerrors; // data to be saved into {workshopform_numerrors} 00165 $mappings = $data->mappings; // data to be saved into {workshopform_numerrors_map} 00166 $todelete = array(); // dimension ids to be deleted 00167 $maxnonegative = 0; // maximum number of (weighted) negative responses 00168 00169 for ($i=0; $i < $norepeats; $i++) { 00170 $record = $records[$i]; 00171 if (0 == strlen(trim($record->description_editor['text']))) { 00172 if (!empty($record->id)) { 00173 // existing dimension record with empty description - to be deleted 00174 $todelete[] = $record->id; 00175 } 00176 continue; 00177 } 00178 if (empty($record->id)) { 00179 // new field 00180 $record->id = $DB->insert_record('workshopform_numerrors', $record); 00181 } else { 00182 // exiting field 00183 $DB->update_record('workshopform_numerrors', $record); 00184 } 00185 $maxnonegative += $record->weight; 00186 // re-save with correct path to embeded media files 00187 $record = file_postupdate_standard_editor($record, 'description', $this->descriptionopts, $PAGE->context, 00188 'workshopform_numerrors', 'description', $record->id); 00189 $DB->update_record('workshopform_numerrors', $record); 00190 } 00191 $this->delete_dimensions($todelete); 00192 00193 // re-save the mappings 00194 $todelete = array(); 00195 foreach ($data->mappings as $nonegative => $grade) { 00196 if (is_null($grade)) { 00197 // no grade set for this number of negative responses 00198 $todelete[] = $nonegative; 00199 continue; 00200 } 00201 if (isset($this->mappings[$nonegative])) { 00202 $DB->set_field('workshopform_numerrors_map', 'grade', $grade, 00203 array('workshopid' => $this->workshop->id, 'nonegative' => $nonegative)); 00204 } else { 00205 $DB->insert_record('workshopform_numerrors_map', 00206 (object)array('workshopid' => $this->workshop->id, 'nonegative' => $nonegative, 'grade' => $grade)); 00207 } 00208 } 00209 // clear mappings that are not valid any more 00210 if (!empty($todelete)) { 00211 list($insql, $params) = $DB->get_in_or_equal($todelete, SQL_PARAMS_NAMED); 00212 $insql = "nonegative $insql OR "; 00213 } else { 00214 $insql = ''; 00215 } 00216 $sql = "DELETE FROM {workshopform_numerrors_map} 00217 WHERE (($insql nonegative > :maxnonegative) AND (workshopid = :workshopid))"; 00218 $params['maxnonegative'] = $maxnonegative; 00219 $params['workshopid'] = $this->workshop->id; 00220 $DB->execute($sql, $params); 00221 } 00222 00232 public function get_assessment_form(moodle_url $actionurl=null, $mode='preview', stdclass $assessment=null, $editable=true, $options=array()) { 00233 global $CFG; // needed because the included files use it 00234 global $PAGE; 00235 global $DB; 00236 require_once(dirname(__FILE__) . '/assessment_form.php'); 00237 00238 $fields = $this->prepare_form_fields($this->dimensions, $this->mappings); 00239 $nodimensions = count($this->dimensions); 00240 00241 // rewrite URLs to the embeded files 00242 for ($i = 0; $i < $nodimensions; $i++) { 00243 $fields->{'description__idx_'.$i} = file_rewrite_pluginfile_urls($fields->{'description__idx_'.$i}, 00244 'pluginfile.php', $PAGE->context->id, 'workshopform_numerrors', 'description', $fields->{'dimensionid__idx_'.$i}); 00245 } 00246 00247 if ('assessment' === $mode and !empty($assessment)) { 00248 // load the previously saved assessment data 00249 $grades = $this->get_current_assessment_data($assessment); 00250 $current = new stdclass(); 00251 for ($i = 0; $i < $nodimensions; $i++) { 00252 $dimid = $fields->{'dimensionid__idx_'.$i}; 00253 if (isset($grades[$dimid])) { 00254 $current->{'gradeid__idx_'.$i} = $grades[$dimid]->id; 00255 $current->{'grade__idx_'.$i} = ($grades[$dimid]->grade == 0 ? -1 : 1); 00256 $current->{'peercomment__idx_'.$i} = $grades[$dimid]->peercomment; 00257 } 00258 } 00259 } 00260 00261 // set up the required custom data common for all strategies 00262 $customdata['workshop'] = $this->workshop; 00263 $customdata['strategy'] = $this; 00264 $customdata['mode'] = $mode; 00265 $customdata['options'] = $options; 00266 00267 // set up strategy-specific custom data 00268 $customdata['nodims'] = $nodimensions; 00269 $customdata['fields'] = $fields; 00270 $customdata['current'] = isset($current) ? $current : null; 00271 $attributes = array('class' => 'assessmentform numerrors'); 00272 00273 return new workshop_numerrors_assessment_form($actionurl, $customdata, 'post', '', $attributes, $editable); 00274 } 00275 00285 public function save_assessment(stdclass $assessment, stdclass $data) { 00286 global $DB; 00287 00288 if (!isset($data->nodims)) { 00289 throw new coding_exception('You did not send me the number of assessment dimensions to process'); 00290 } 00291 for ($i = 0; $i < $data->nodims; $i++) { 00292 $grade = new stdclass(); 00293 $grade->id = $data->{'gradeid__idx_' . $i}; 00294 $grade->assessmentid = $assessment->id; 00295 $grade->strategy = 'numerrors'; 00296 $grade->dimensionid = $data->{'dimensionid__idx_' . $i}; 00297 $grade->grade = ($data->{'grade__idx_' . $i} <= 0 ? 0 : 1); 00298 $grade->peercomment = $data->{'peercomment__idx_' . $i}; 00299 $grade->peercommentformat = FORMAT_HTML; 00300 if (empty($grade->id)) { 00301 // new grade 00302 $grade->id = $DB->insert_record('workshop_grades', $grade); 00303 } else { 00304 // updated grade 00305 $DB->update_record('workshop_grades', $grade); 00306 } 00307 } 00308 return $this->update_peer_grade($assessment); 00309 } 00310 00316 public function form_ready() { 00317 if (count($this->dimensions) > 0) { 00318 return true; 00319 } 00320 return false; 00321 } 00322 00326 public function get_assessments_recordset($restrict=null) { 00327 global $DB; 00328 00329 $sql = 'SELECT s.id AS submissionid, 00330 a.id AS assessmentid, a.weight AS assessmentweight, a.reviewerid, a.gradinggrade, 00331 g.dimensionid, g.grade 00332 FROM {workshop_submissions} s 00333 JOIN {workshop_assessments} a ON (a.submissionid = s.id) 00334 JOIN {workshop_grades} g ON (g.assessmentid = a.id AND g.strategy = :strategy) 00335 WHERE s.example=0 AND s.workshopid=:workshopid'; // to be cont. 00336 $params = array('workshopid' => $this->workshop->id, 'strategy' => $this->workshop->strategy); 00337 00338 if (is_null($restrict)) { 00339 // update all users - no more conditions 00340 } elseif (!empty($restrict)) { 00341 list($usql, $uparams) = $DB->get_in_or_equal($restrict, SQL_PARAMS_NAMED); 00342 $sql .= " AND a.reviewerid $usql"; 00343 $params = array_merge($params, $uparams); 00344 } else { 00345 throw new coding_exception('Empty value is not a valid parameter here'); 00346 } 00347 00348 $sql .= ' ORDER BY s.id'; // this is important for bulk processing 00349 00350 return $DB->get_recordset_sql($sql, $params); 00351 00352 } 00353 00357 public function get_dimensions_info() { 00358 global $DB; 00359 00360 $params = array('workshopid' => $this->workshop->id); 00361 $dimrecords = $DB->get_records('workshopform_numerrors', array('workshopid' => $this->workshop->id), 'sort', 'id,weight'); 00362 foreach ($dimrecords as $dimid => $dimrecord) { 00363 $dimrecords[$dimid]->min = 0; 00364 $dimrecords[$dimid]->max = 1; 00365 } 00366 return $dimrecords; 00367 } 00368 00378 public static function scale_used($scaleid, $workshopid=null) { 00379 return false; 00380 } 00381 00389 public static function delete_instance($workshopid) { 00390 global $DB; 00391 00392 $DB->delete_records('workshopform_numerrors', array('workshopid' => $workshopid)); 00393 $DB->delete_records('workshopform_numerrors_map', array('workshopid' => $workshopid)); 00394 } 00395 00397 // Internal methods // 00399 00405 protected function load_fields() { 00406 global $DB; 00407 00408 $sql = 'SELECT * 00409 FROM {workshopform_numerrors} 00410 WHERE workshopid = :workshopid 00411 ORDER BY sort'; 00412 $params = array('workshopid' => $this->workshop->id); 00413 00414 return $DB->get_records_sql($sql, $params); 00415 } 00416 00422 protected function load_mappings() { 00423 global $DB; 00424 return $DB->get_records('workshopform_numerrors_map', array('workshopid' => $this->workshop->id), 'nonegative', 00425 'nonegative,grade'); // we can use nonegative as key here as it must be unique within workshop 00426 } 00427 00435 protected function prepare_form_fields(array $dims, array $maps) { 00436 00437 $formdata = new stdclass(); 00438 $key = 0; 00439 foreach ($dims as $dimension) { 00440 $formdata->{'dimensionid__idx_' . $key} = $dimension->id; 00441 $formdata->{'description__idx_' . $key} = $dimension->description; 00442 $formdata->{'description__idx_' . $key.'format'} = $dimension->descriptionformat; 00443 $formdata->{'grade0__idx_' . $key} = $dimension->grade0; 00444 $formdata->{'grade1__idx_' . $key} = $dimension->grade1; 00445 $formdata->{'weight__idx_' . $key} = $dimension->weight; 00446 $key++; 00447 } 00448 00449 foreach ($maps as $nonegative => $map) { 00450 $formdata->{'map__idx_' . $nonegative} = $map->grade; 00451 } 00452 00453 return $formdata; 00454 } 00455 00464 protected function delete_dimensions(array $ids) { 00465 global $DB, $PAGE; 00466 00467 $fs = get_file_storage(); 00468 foreach ($ids as $id) { 00469 $fs->delete_area_files($PAGE->context->id, 'workshopform_numerrors', 'description', $id); 00470 } 00471 $DB->delete_records_list('workshopform_numerrors', 'id', $ids); 00472 } 00473 00485 protected function prepare_database_fields(stdclass $raw) { 00486 global $PAGE; 00487 00488 $cook = new stdclass(); // to be returned 00489 $cook->numerrors = array(); // to be stored in {workshopform_numerrors} 00490 $cook->mappings = array(); // to be stored in {workshopform_numerrors_map} 00491 00492 for ($i = 0; $i < $raw->norepeats; $i++) { 00493 $cook->numerrors[$i] = new stdclass(); 00494 $cook->numerrors[$i]->id = $raw->{'dimensionid__idx_'.$i}; 00495 $cook->numerrors[$i]->workshopid = $this->workshop->id; 00496 $cook->numerrors[$i]->sort = $i + 1; 00497 $cook->numerrors[$i]->description_editor = $raw->{'description__idx_'.$i.'_editor'}; 00498 $cook->numerrors[$i]->grade0 = $raw->{'grade0__idx_'.$i}; 00499 $cook->numerrors[$i]->grade1 = $raw->{'grade1__idx_'.$i}; 00500 $cook->numerrors[$i]->weight = $raw->{'weight__idx_'.$i}; 00501 } 00502 00503 $i = 1; 00504 while (isset($raw->{'map__idx_'.$i})) { 00505 if (is_numeric($raw->{'map__idx_'.$i})) { 00506 $cook->mappings[$i] = $raw->{'map__idx_'.$i}; // should be a value from 0 to 100 00507 } else { 00508 $cook->mappings[$i] = null; // the user did not set anything 00509 } 00510 $i++; 00511 } 00512 00513 return $cook; 00514 } 00515 00522 protected function get_current_assessment_data(stdclass $assessment) { 00523 global $DB; 00524 00525 if (empty($this->dimensions)) { 00526 return array(); 00527 } 00528 list($dimsql, $dimparams) = $DB->get_in_or_equal(array_keys($this->dimensions), SQL_PARAMS_NAMED); 00529 // beware! the caller may rely on the returned array is indexed by dimensionid 00530 $sql = "SELECT dimensionid, wg.* 00531 FROM {workshop_grades} wg 00532 WHERE assessmentid = :assessmentid AND strategy= :strategy AND dimensionid $dimsql"; 00533 $params = array('assessmentid' => $assessment->id, 'strategy' => 'numerrors'); 00534 $params = array_merge($params, $dimparams); 00535 00536 return $DB->get_records_sql($sql, $params); 00537 } 00538 00545 protected function update_peer_grade(stdclass $assessment) { 00546 $grades = $this->get_current_assessment_data($assessment); 00547 $suggested = $this->calculate_peer_grade($grades); 00548 if (!is_null($suggested)) { 00549 $this->workshop->set_peer_grade($assessment->id, $suggested); 00550 } 00551 return $suggested; 00552 } 00553 00560 protected function calculate_peer_grade(array $grades) { 00561 if (empty($grades)) { 00562 return null; 00563 } 00564 $sumerrors = 0; // sum of the weighted errors (i.e. the negative responses) 00565 foreach ($grades as $grade) { 00566 if (grade_floats_different($grade->grade, 1.00000)) { 00567 // negative reviewer's response 00568 $sumerrors += $this->dimensions[$grade->dimensionid]->weight; 00569 } 00570 } 00571 return $this->errors_to_grade($sumerrors); 00572 } 00573 00593 protected function errors_to_grade($numerrors) { 00594 $grade = 100.00000; 00595 for ($i = 1; $i <= $numerrors; $i++) { 00596 if (isset($this->mappings[$i])) { 00597 $grade = $this->mappings[$i]->grade; 00598 } 00599 } 00600 if ($grade > 100.00000) { 00601 $grade = 100.00000; 00602 } 00603 if ($grade < 0.00000) { 00604 $grade = 0.00000; 00605 } 00606 return grade_floatval($grade); 00607 } 00608 }