Commit | Line | Data |
---|---|---|
9b8550f8 DM |
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 | * Grading method controller for the Rubric plugin | |
20 | * | |
21 | * @package gradingform | |
22 | * @subpackage rubric | |
23 | * @copyright 2011 David Mudrak <david@moodle.com> | |
24 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
25 | */ | |
26 | ||
27 | defined('MOODLE_INTERNAL') || die(); | |
28 | ||
fe817d87 | 29 | require_once($CFG->dirroot.'/grade/grading/form/lib.php'); |
9b8550f8 DM |
30 | |
31 | /** | |
32 | * This controller encapsulates the rubric grading logic | |
33 | */ | |
21d37aa6 | 34 | class gradingform_rubric_controller extends gradingform_controller { |
ab156741 MG |
35 | // Modes of displaying the rubric (used in gradingform_rubric_renderer) |
36 | const DISPLAY_EDIT_FULL = 1; // For editing (moderator or teacher creates a rubric) | |
37 | const DISPLAY_EDIT_FROZEN = 2; // Preview the rubric design with hidden fields | |
38 | const DISPLAY_PREVIEW = 3; // Preview the rubric design | |
39 | const DISPLAY_EVAL = 4; // For evaluation, enabled (teacher grades a student) | |
40 | const DISPLAY_EVAL_FROZEN = 5; // For evaluation, with hidden fields | |
39c6f4b6 MG |
41 | const DISPLAY_REVIEW = 6; // Teacher reviews filled rubric |
42 | const DISPLAY_VIEW = 7; // Dispaly filled rubric (i.e. students see their grades) | |
9b8550f8 DM |
43 | |
44 | /** | |
45 | * Extends the module settings navigation with the rubric grading settings | |
46 | * | |
47 | * This function is called when the context for the page is an activity module with the | |
48 | * FEATURE_ADVANCED_GRADING, the user has the permission moodle/grade:managegradingforms | |
49 | * and there is an area with the active grading method set to 'rubric'. | |
50 | * | |
51 | * @param settings_navigation $settingsnav {@link settings_navigation} | |
52 | * @param navigation_node $node {@link navigation_node} | |
53 | */ | |
54 | public function extend_settings_navigation(settings_navigation $settingsnav, navigation_node $node=null) { | |
55 | $node->add(get_string('definerubric', 'gradingform_rubric'), | |
6832a102 | 56 | $this->get_editor_url(), settings_navigation::TYPE_CUSTOM, |
9b8550f8 DM |
57 | null, null, new pix_icon('icon', '', 'gradingform_rubric')); |
58 | } | |
21d37aa6 | 59 | |
c586d2bf | 60 | /** |
fe817d87 | 61 | * Saves the rubric definition into the database |
c586d2bf | 62 | * |
fe817d87 | 63 | * @see parent::update_definition() |
9e2eca0f | 64 | * @param stdClass $newdefinition rubric definition data as coming from gradingform_rubric_editrubric::get_data() |
fe817d87 | 65 | * @param int|null $usermodified optional userid of the author of the definition, defaults to the current user |
c586d2bf | 66 | */ |
3599b113 | 67 | public function update_definition(stdClass $newdefinition, $usermodified = null) { |
0136124e MG |
68 | $this->update_or_check_rubric($newdefinition, $usermodified, true); |
69 | if (isset($newdefinition->rubric['regrade']) && $newdefinition->rubric['regrade']) { | |
70 | $this->mark_for_regrade(); | |
71 | } | |
72 | } | |
73 | ||
74 | /** | |
75 | * Either saves the rubric definition into the database or check if it has been changed. | |
76 | * Returns the level of changes: | |
77 | * 0 - no changes | |
78 | * 1 - only texts or criteria sortorders are changed, students probably do not require re-grading | |
79 | * 2 - added levels but maximum score on rubric is the same, students still may not require re-grading | |
80 | * 3 - removed criteria or added levels or changed number of points, students require re-grading but may be re-graded automatically | |
81 | * 4 - removed levels - students require re-grading and not all students may be re-graded automatically | |
82 | * 5 - added criteria - all students require manual re-grading | |
83 | * | |
84 | * @param stdClass $newdefinition rubric definition data as coming from gradingform_rubric_editrubric::get_data() | |
85 | * @param int|null $usermodified optional userid of the author of the definition, defaults to the current user | |
86 | * @param boolean $doupdate if true actually updates DB, otherwise performs a check | |
87 | * | |
88 | */ | |
89 | public function update_or_check_rubric(stdClass $newdefinition, $usermodified = null, $doupdate = false) { | |
c586d2bf | 90 | global $DB; |
c586d2bf | 91 | |
fe817d87 | 92 | // firstly update the common definition data in the {grading_definition} table |
9e2eca0f | 93 | if ($this->definition === false) { |
0136124e MG |
94 | if (!$doupdate) { |
95 | // if we create the new definition there is no such thing as re-grading anyway | |
96 | return 5; | |
97 | } | |
7622ae95 | 98 | // if definition does not exist yet, create a blank one |
9e2eca0f | 99 | // (we need id to save files embedded in description) |
3599b113 | 100 | parent::update_definition(new stdClass(), $usermodified); |
9e2eca0f MG |
101 | parent::load_definition(); |
102 | } | |
39c6f4b6 MG |
103 | if (!isset($newdefinition->rubric['options'])) { |
104 | $newdefinition->rubric['options'] = self::get_default_options(); | |
105 | } | |
106 | $newdefinition->options = json_encode($newdefinition->rubric['options']); | |
107 | $editoroptions = self::description_form_field_options($this->get_context()); | |
108 | $newdefinition = file_postupdate_standard_editor($newdefinition, 'description', $editoroptions, $this->get_context(), | |
b2c16c6e | 109 | 'grading', 'description', $this->definition->id); |
9e2eca0f | 110 | |
fe817d87 | 111 | // reload the definition from the database |
9e2eca0f | 112 | $currentdefinition = $this->get_definition(true); |
fe817d87 | 113 | |
9e2eca0f | 114 | // update rubric data |
0136124e | 115 | $haschanges = array(); |
39c6f4b6 | 116 | if (empty($newdefinition->rubric['criteria'])) { |
fe817d87 | 117 | $newcriteria = array(); |
c586d2bf | 118 | } else { |
39c6f4b6 | 119 | $newcriteria = $newdefinition->rubric['criteria']; // new ones to be saved |
c586d2bf | 120 | } |
fe817d87 DM |
121 | $currentcriteria = $currentdefinition->rubric_criteria; |
122 | $criteriafields = array('sortorder', 'description', 'descriptionformat'); | |
123 | $levelfields = array('score', 'definition', 'definitionformat'); | |
124 | foreach ($newcriteria as $id => $criterion) { | |
125 | // get list of submitted levels | |
126 | $levelsdata = array(); | |
127 | if (array_key_exists('levels', $criterion)) { | |
128 | $levelsdata = $criterion['levels']; | |
129 | } | |
0136124e | 130 | $criterionmaxscore = null; |
fe817d87 DM |
131 | if (preg_match('/^NEWID\d+$/', $id)) { |
132 | // insert criterion into DB | |
71ab436a | 133 | $data = array('definitionid' => $this->definition->id, 'descriptionformat' => FORMAT_MOODLE); // TODO format is not supported yet |
fe817d87 DM |
134 | foreach ($criteriafields as $key) { |
135 | if (array_key_exists($key, $criterion)) { | |
136 | $data[$key] = $criterion[$key]; | |
137 | } | |
138 | } | |
0136124e MG |
139 | if ($doupdate) { |
140 | $id = $DB->insert_record('gradingform_rubric_criteria', $data); | |
141 | } | |
142 | $haschanges[5] = true; | |
fe817d87 DM |
143 | } else { |
144 | // update criterion in DB | |
145 | $data = array(); | |
146 | foreach ($criteriafields as $key) { | |
147 | if (array_key_exists($key, $criterion) && $criterion[$key] != $currentcriteria[$id][$key]) { | |
148 | $data[$key] = $criterion[$key]; | |
149 | } | |
150 | } | |
151 | if (!empty($data)) { | |
152 | // update only if something is changed | |
153 | $data['id'] = $id; | |
0136124e MG |
154 | if ($doupdate) { |
155 | $DB->update_record('gradingform_rubric_criteria', $data); | |
156 | } | |
157 | $haschanges[1] = true; | |
c586d2bf | 158 | } |
0136124e MG |
159 | // remove deleted levels from DB and calculate the maximum score for this criteria |
160 | foreach ($currentcriteria[$id]['levels'] as $levelid => $currentlevel) { | |
161 | if ($criterionmaxscore === null || $criterionmaxscore < $currentlevel['score']) { | |
162 | $criterionmaxscore = $currentlevel['score']; | |
163 | } | |
fe817d87 | 164 | if (!array_key_exists($levelid, $levelsdata)) { |
0136124e MG |
165 | if ($doupdate) { |
166 | $DB->delete_records('gradingform_rubric_levels', array('id' => $levelid)); | |
167 | } | |
168 | $haschanges[4] = true; | |
fe817d87 DM |
169 | } |
170 | } | |
171 | } | |
172 | foreach ($levelsdata as $levelid => $level) { | |
2ae7faf1 MG |
173 | if (isset($level['score'])) { |
174 | $level['score'] = (float)$level['score']; | |
175 | if ($level['score']<0) { | |
176 | // TODO why we can't allow negative score for rubric? | |
177 | $level['score'] = 0; | |
178 | } | |
179 | } | |
fe817d87 DM |
180 | if (preg_match('/^NEWID\d+$/', $levelid)) { |
181 | // insert level into DB | |
182 | $data = array('criterionid' => $id, 'definitionformat' => FORMAT_MOODLE); // TODO format is not supported yet | |
183 | foreach ($levelfields as $key) { | |
184 | if (array_key_exists($key, $level)) { | |
185 | $data[$key] = $level[$key]; | |
c586d2bf MG |
186 | } |
187 | } | |
0136124e MG |
188 | if ($doupdate) { |
189 | $levelid = $DB->insert_record('gradingform_rubric_levels', $data); | |
190 | } | |
191 | if ($criterionmaxscore !== null && $criterionmaxscore >= $level['score']) { | |
192 | // new level is added but the maximum score for this criteria did not change, re-grading may not be necessary | |
193 | $haschanges[2] = true; | |
194 | } else { | |
195 | $haschanges[3] = true; | |
196 | } | |
c586d2bf | 197 | } else { |
fe817d87 | 198 | // update level in DB |
c586d2bf | 199 | $data = array(); |
fe817d87 DM |
200 | foreach ($levelfields as $key) { |
201 | if (array_key_exists($key, $level) && $level[$key] != $currentcriteria[$id]['levels'][$levelid][$key]) { | |
202 | $data[$key] = $level[$key]; | |
c586d2bf MG |
203 | } |
204 | } | |
205 | if (!empty($data)) { | |
206 | // update only if something is changed | |
fe817d87 | 207 | $data['id'] = $levelid; |
0136124e MG |
208 | if ($doupdate) { |
209 | $DB->update_record('gradingform_rubric_levels', $data); | |
210 | } | |
211 | if (isset($data['score'])) { | |
212 | $haschanges[3] = true; | |
213 | } | |
214 | $haschanges[1] = true; | |
c586d2bf MG |
215 | } |
216 | } | |
217 | } | |
c586d2bf | 218 | } |
fe817d87 DM |
219 | // remove deleted criteria from DB |
220 | foreach (array_keys($currentcriteria) as $id) { | |
221 | if (!array_key_exists($id, $newcriteria)) { | |
0136124e MG |
222 | if ($doupdate) { |
223 | $DB->delete_records('gradingform_rubric_criteria', array('id' => $id)); | |
224 | $DB->delete_records('gradingform_rubric_levels', array('criterionid' => $id)); | |
225 | } | |
226 | $haschanges[3] = true; | |
fe817d87 | 227 | } |
c586d2bf | 228 | } |
0136124e MG |
229 | foreach (array('status', 'description', 'descriptionformat', 'name', 'options') as $key) { |
230 | if (isset($newdefinition->$key) && $newdefinition->$key != $this->definition->$key) { | |
231 | $haschanges[1] = true; | |
232 | } | |
233 | } | |
234 | if ($usermodified && $usermodified != $this->definition->usermodified) { | |
235 | $haschanges[1] = true; | |
236 | } | |
237 | if (!count($haschanges)) { | |
238 | return 0; | |
239 | } | |
240 | if ($doupdate) { | |
241 | parent::update_definition($newdefinition, $usermodified); | |
242 | $this->load_definition(); | |
243 | } | |
244 | // return the maximum level of changes | |
245 | $changelevels = array_keys($haschanges); | |
246 | sort($changelevels); | |
247 | return array_pop($changelevels); | |
248 | } | |
249 | ||
250 | public function mark_for_regrade() { | |
251 | global $DB; | |
252 | if ($this->has_active_instances()) { | |
71ab436a | 253 | $conditions = array('definitionid' => $this->definition->id, |
0136124e MG |
254 | 'status' => gradingform_instance::INSTANCE_STATUS_ACTIVE); |
255 | $DB->set_field('grading_instances', 'status', gradingform_instance::INSTANCE_STATUS_NEEDUPDATE, $conditions); | |
256 | } | |
c586d2bf MG |
257 | } |
258 | ||
259 | /** | |
fe817d87 | 260 | * Loads the rubric form definition if it exists |
c586d2bf | 261 | * |
fe817d87 | 262 | * There is a new array called 'rubric_criteria' appended to the list of parent's definition properties. |
c586d2bf | 263 | */ |
fe817d87 | 264 | protected function load_definition() { |
c586d2bf | 265 | global $DB; |
fe817d87 DM |
266 | $sql = "SELECT gd.*, |
267 | rc.id AS rcid, rc.sortorder AS rcsortorder, rc.description AS rcdescription, rc.descriptionformat AS rcdescriptionformat, | |
268 | rl.id AS rlid, rl.score AS rlscore, rl.definition AS rldefinition, rl.definitionformat AS rldefinitionformat | |
269 | FROM {grading_definitions} gd | |
71ab436a | 270 | LEFT JOIN {gradingform_rubric_criteria} rc ON (rc.definitionid = gd.id) |
fe817d87 DM |
271 | LEFT JOIN {gradingform_rubric_levels} rl ON (rl.criterionid = rc.id) |
272 | WHERE gd.areaid = :areaid AND gd.method = :method | |
273 | ORDER BY rc.sortorder,rl.score"; | |
274 | $params = array('areaid' => $this->areaid, 'method' => $this->get_method_name()); | |
275 | ||
276 | $rs = $DB->get_recordset_sql($sql, $params); | |
277 | $this->definition = false; | |
278 | foreach ($rs as $record) { | |
279 | // pick the common definition data | |
5060997b | 280 | if ($this->definition === false) { |
fe817d87 DM |
281 | $this->definition = new stdClass(); |
282 | foreach (array('id', 'name', 'description', 'descriptionformat', 'status', 'copiedfromid', | |
3f3ee711 | 283 | 'timecreated', 'usercreated', 'timemodified', 'usermodified', 'timecopied', 'options') as $fieldname) { |
fe817d87 DM |
284 | $this->definition->$fieldname = $record->$fieldname; |
285 | } | |
286 | $this->definition->rubric_criteria = array(); | |
287 | } | |
288 | // pick the criterion data | |
289 | if (!empty($record->rcid) and empty($this->definition->rubric_criteria[$record->rcid])) { | |
290 | foreach (array('id', 'sortorder', 'description', 'descriptionformat') as $fieldname) { | |
291 | $this->definition->rubric_criteria[$record->rcid][$fieldname] = $record->{'rc'.$fieldname}; | |
292 | } | |
293 | $this->definition->rubric_criteria[$record->rcid]['levels'] = array(); | |
c586d2bf | 294 | } |
fe817d87 DM |
295 | // pick the level data |
296 | if (!empty($record->rlid)) { | |
297 | foreach (array('id', 'score', 'definition', 'definitionformat') as $fieldname) { | |
2ae7faf1 MG |
298 | $value = $record->{'rl'.$fieldname}; |
299 | if ($fieldname == 'score') { | |
300 | $value = (float)$value; // To prevent display like 1.00000 | |
301 | } | |
302 | $this->definition->rubric_criteria[$record->rcid]['levels'][$record->rlid][$fieldname] = $value; | |
c586d2bf MG |
303 | } |
304 | } | |
305 | } | |
fe817d87 | 306 | $rs->close(); |
39c6f4b6 MG |
307 | $options = $this->get_options(); |
308 | if (!$options['sortlevelsasc']) { | |
309 | foreach (array_keys($this->definition->rubric_criteria) as $rcid) { | |
310 | $this->definition->rubric_criteria[$rcid]['levels'] = array_reverse($this->definition->rubric_criteria[$rcid]['levels'], true); | |
311 | } | |
312 | } | |
313 | } | |
314 | ||
315 | public static function get_default_options() { | |
316 | $options = array( | |
317 | 'sortlevelsasc' => 1, | |
318 | //'showdescriptionteacher' => 1, | |
319 | //'showdescriptionstudent' => 1, | |
320 | 'showscoreteacher' => 1, | |
321 | 'showscorestudent' => 1, | |
322 | 'enableremarks' => 1, | |
323 | 'showremarksstudent' => 1 | |
324 | ); | |
325 | // TODO description options | |
326 | return $options; | |
327 | } | |
328 | ||
329 | public function get_options() { | |
330 | $options = self::get_default_options(); | |
331 | if (!empty($this->definition->options)) { | |
332 | $thisoptions = json_decode($this->definition->options); | |
333 | foreach ($thisoptions as $option => $value) { | |
334 | $options[$option] = $value; | |
335 | } | |
336 | } | |
337 | return $options; | |
c586d2bf MG |
338 | } |
339 | ||
340 | /** | |
fe817d87 | 341 | * Converts the current definition into an object suitable for the editor form's set_data() |
c586d2bf | 342 | * |
fe817d87 | 343 | * @return stdClass |
c586d2bf | 344 | */ |
fe817d87 DM |
345 | public function get_definition_for_editing() { |
346 | ||
347 | $definition = $this->get_definition(); | |
c586d2bf | 348 | $properties = new stdClass(); |
fe817d87 DM |
349 | $properties->areaid = $this->areaid; |
350 | if ($definition) { | |
39c6f4b6 | 351 | foreach (array('id', 'name', 'description', 'descriptionformat', 'status') as $key) { |
fe817d87 | 352 | $properties->$key = $definition->$key; |
c586d2bf | 353 | } |
c586d2bf | 354 | $options = self::description_form_field_options($this->get_context()); |
fe817d87 | 355 | $properties = file_prepare_standard_editor($properties, 'description', $options, $this->get_context(), |
b2c16c6e | 356 | 'grading', 'description', $definition->id); |
fe817d87 | 357 | } |
39c6f4b6 | 358 | $properties->rubric = array('criteria' => array(), 'options' => $this->get_options()); |
fe817d87 | 359 | if (!empty($definition->rubric_criteria)) { |
39c6f4b6 | 360 | $properties->rubric['criteria'] = $definition->rubric_criteria; |
c586d2bf | 361 | } |
c586d2bf | 362 | |
c586d2bf MG |
363 | return $properties; |
364 | } | |
365 | ||
fde33804 DM |
366 | /** |
367 | * Returns the form definition suitable for cloning into another area | |
368 | * | |
369 | * @see parent::get_definition_copy() | |
370 | * @param gradingform_controller $target the controller of the new copy | |
371 | * @return stdClass definition structure to pass to the target's {@link update_definition()} | |
372 | */ | |
373 | public function get_definition_copy(gradingform_controller $target) { | |
374 | ||
375 | $new = parent::get_definition_copy($target); | |
8722a322 MG |
376 | $old = $this->get_definition_for_editing(); |
377 | $new->description_editor = $old->description_editor; | |
39c6f4b6 | 378 | $new->rubric = array('criteria' => array(), 'options' => $old->rubric['options']); |
fde33804 DM |
379 | $newcritid = 1; |
380 | $newlevid = 1; | |
39c6f4b6 | 381 | foreach ($old->rubric['criteria'] as $oldcritid => $oldcrit) { |
fde33804 DM |
382 | unset($oldcrit['id']); |
383 | if (isset($oldcrit['levels'])) { | |
384 | foreach ($oldcrit['levels'] as $oldlevid => $oldlev) { | |
385 | unset($oldlev['id']); | |
386 | $oldcrit['levels']['NEWID'.$newlevid] = $oldlev; | |
387 | unset($oldcrit['levels'][$oldlevid]); | |
388 | $newlevid++; | |
389 | } | |
390 | } else { | |
391 | $oldcrit['levels'] = array(); | |
392 | } | |
39c6f4b6 | 393 | $new->rubric['criteria']['NEWID'.$newcritid] = $oldcrit; |
fde33804 DM |
394 | $newcritid++; |
395 | } | |
396 | ||
397 | return $new; | |
398 | } | |
399 | ||
fe817d87 DM |
400 | /** |
401 | * @return array options for the form description field | |
402 | */ | |
c586d2bf MG |
403 | public static function description_form_field_options($context) { |
404 | global $CFG; | |
405 | return array( | |
406 | 'maxfiles' => -1, | |
407 | 'maxbytes' => get_max_upload_file_size($CFG->maxbytes), | |
fe817d87 | 408 | 'context' => $context, |
c586d2bf MG |
409 | ); |
410 | } | |
411 | ||
3599b113 MG |
412 | /** |
413 | * Formats the definition description for display on page | |
414 | * | |
415 | * @return string | |
416 | */ | |
c586d2bf | 417 | public function get_formatted_description() { |
3599b113 MG |
418 | if (!isset($this->definition->description)) { |
419 | return ''; | |
c586d2bf MG |
420 | } |
421 | $context = $this->get_context(); | |
422 | ||
423 | $options = self::description_form_field_options($this->get_context()); | |
fe817d87 | 424 | $description = file_rewrite_pluginfile_urls($this->definition->description, 'pluginfile.php', $context->id, |
b2c16c6e | 425 | 'grading', 'description', $this->definition->id, $options); |
c586d2bf MG |
426 | |
427 | $formatoptions = array( | |
428 | 'noclean' => false, | |
429 | 'trusted' => false, | |
430 | 'filter' => true, | |
431 | 'context' => $context | |
432 | ); | |
433 | return format_text($description, $this->definition->descriptionformat, $formatoptions); | |
434 | } | |
435 | ||
6832a102 DM |
436 | /** |
437 | * Returns the rubric plugin renderer | |
438 | * | |
439 | * @param moodle_page $page the target page | |
440 | * @return renderer_base | |
441 | */ | |
442 | public function get_renderer(moodle_page $page) { | |
443 | return $page->get_renderer('gradingform_'. $this->get_method_name()); | |
444 | } | |
445 | ||
446 | /** | |
447 | * Returns the HTML code displaying the preview of the grading form | |
448 | * | |
449 | * @param moodle_page $page the target page | |
450 | * @return string | |
451 | */ | |
452 | public function render_preview(moodle_page $page) { | |
453 | ||
20836db9 DM |
454 | if (!$this->is_form_defined()) { |
455 | throw new coding_exception('It is the caller\'s responsibility to make sure that the form is actually defined'); | |
456 | } | |
6832a102 | 457 | |
6832a102 | 458 | $output = $this->get_renderer($page); |
36937f02 | 459 | $criteria = $this->definition->rubric_criteria; |
39c6f4b6 MG |
460 | $options = $this->get_options(); |
461 | $rubric = $output->display_rubric($criteria, $options, self::DISPLAY_PREVIEW, 'rubric'); | |
6832a102 | 462 | |
20836db9 | 463 | return $rubric; |
6832a102 | 464 | } |
671ec8f5 DM |
465 | |
466 | /** | |
467 | * Deletes the rubric definition and all the associated information | |
468 | */ | |
469 | protected function delete_plugin_definition() { | |
470 | global $DB; | |
471 | ||
472 | // get the list of instances | |
71ab436a | 473 | $instances = array_keys($DB->get_records('grading_instances', array('definitionid' => $this->definition->id), '', 'id')); |
671ec8f5 DM |
474 | // delete all fillings |
475 | $DB->delete_records_list('gradingform_rubric_fillings', 'forminstanceid', $instances); | |
476 | // delete instances | |
477 | $DB->delete_records_list('grading_instances', 'id', $instances); | |
478 | // get the list of criteria records | |
71ab436a | 479 | $criteria = array_keys($DB->get_records('gradingform_rubric_criteria', array('definitionid' => $this->definition->id), '', 'id')); |
671ec8f5 DM |
480 | // delete levels |
481 | $DB->delete_records_list('gradingform_rubric_levels', 'criterionid', $criteria); | |
482 | // delete critera | |
483 | $DB->delete_records_list('gradingform_rubric_criteria', 'id', $criteria); | |
484 | } | |
36937f02 | 485 | |
188e840b MG |
486 | /** |
487 | * If instanceid is specified and grading instance exists and it is created by this rater for | |
488 | * this item, this instance is returned. | |
489 | * If there exists a draft for this raterid+itemid, take this draft (this is the change from parent) | |
490 | * Otherwise new instance is created for the specified rater and itemid | |
491 | * | |
492 | * @param int $instanceid | |
493 | * @param int $raterid | |
494 | * @param int $itemid | |
495 | * @return gradingform_instance | |
496 | */ | |
497 | public function get_or_create_instance($instanceid, $raterid, $itemid) { | |
498 | global $DB; | |
499 | if ($instanceid && | |
500 | $instance = $DB->get_record('grading_instances', array('id' => $instanceid, 'raterid' => $raterid, 'itemid' => $itemid), '*', IGNORE_MISSING)) { | |
501 | return $this->get_instance($instance); | |
502 | } | |
503 | if ($itemid && $raterid) { | |
504 | if ($rs = $DB->get_records('grading_instances', array('raterid' => $raterid, 'itemid' => $itemid), 'timemodified DESC', '*', 0, 1)) { | |
505 | $record = reset($rs); | |
506 | $currentinstance = $this->get_current_instance($raterid, $itemid); | |
050125d4 MG |
507 | if ($record->status == gradingform_rubric_instance::INSTANCE_STATUS_INCOMPLETE && |
508 | (!$currentinstance || $record->timemodified > $currentinstance->get_data('timemodified'))) { | |
188e840b MG |
509 | $record->isrestored = true; |
510 | return $this->get_instance($record); | |
511 | } | |
512 | } | |
513 | } | |
514 | return $this->create_instance($raterid, $itemid); | |
515 | } | |
516 | ||
36937f02 MG |
517 | /** |
518 | * Returns html code to be included in student's feedback. | |
519 | * | |
520 | * @param moodle_page $page | |
521 | * @param int $itemid | |
9e2eca0f | 522 | * @param array $grading_info result of function grade_get_grades |
36937f02 | 523 | * @param string $defaultcontent default string to be returned if no active grading is found |
0136124e | 524 | * @param boolean $cangrade whether current user has capability to grade in this context |
36937f02 MG |
525 | * @return string |
526 | */ | |
0136124e MG |
527 | public function render_grade($page, $itemid, $grading_info, $defaultcontent, $cangrade) { |
528 | return $this->get_renderer($page)->display_instances($this->get_active_instances($itemid), $defaultcontent, $cangrade); | |
36937f02 | 529 | } |
20836db9 DM |
530 | |
531 | //// full-text search support ///////////////////////////////////////////// | |
532 | ||
533 | /** | |
534 | * Prepare the part of the search query to append to the FROM statement | |
535 | * | |
536 | * @param string $gdid the alias of grading_definitions.id column used by the caller | |
537 | * @return string | |
538 | */ | |
539 | public static function sql_search_from_tables($gdid) { | |
71ab436a | 540 | return " LEFT JOIN {gradingform_rubric_criteria} rc ON (rc.definitionid = $gdid) |
20836db9 DM |
541 | LEFT JOIN {gradingform_rubric_levels} rl ON (rl.criterionid = rc.id)"; |
542 | } | |
543 | ||
544 | /** | |
545 | * Prepare the parts of the SQL WHERE statement to search for the given token | |
546 | * | |
547 | * The returned array cosists of the list of SQL comparions and the list of | |
548 | * respective parameters for the comparisons. The returned chunks will be joined | |
549 | * with other conditions using the OR operator. | |
550 | * | |
551 | * @param string $token token to search for | |
552 | * @return array | |
553 | */ | |
554 | public static function sql_search_where($token) { | |
555 | global $DB; | |
556 | ||
557 | $subsql = array(); | |
558 | $params = array(); | |
559 | ||
560 | // search in rubric criteria description | |
561 | $subsql[] = $DB->sql_like('rc.description', '?', false, false); | |
562 | $params[] = '%'.$DB->sql_like_escape($token).'%'; | |
563 | ||
564 | // search in rubric levels definition | |
565 | $subsql[] = $DB->sql_like('rl.definition', '?', false, false); | |
566 | $params[] = '%'.$DB->sql_like_escape($token).'%'; | |
567 | ||
568 | return array($subsql, $params); | |
569 | } | |
9b8550f8 | 570 | } |
36937f02 MG |
571 | |
572 | /** | |
573 | * Class to manage one rubric grading instance. Stores information and performs actions like | |
574 | * update, copy, validate, submit, etc. | |
575 | * | |
576 | * @copyright 2011 Marina Glancy | |
577 | */ | |
578 | class gradingform_rubric_instance extends gradingform_instance { | |
579 | ||
5060997b MG |
580 | protected $rubric; |
581 | ||
36937f02 MG |
582 | /** |
583 | * Deletes this (INCOMPLETE) instance from database. | |
584 | */ | |
585 | public function cancel() { | |
586 | global $DB; | |
587 | parent::cancel(); | |
588 | $DB->delete_records('gradingform_rubric_fillings', array('forminstanceid' => $this->get_id())); | |
589 | } | |
590 | ||
591 | /** | |
592 | * Duplicates the instance before editing (optionally substitutes raterid and/or itemid with | |
593 | * the specified values) | |
594 | * | |
595 | * @param int $raterid value for raterid in the duplicate | |
596 | * @param int $itemid value for itemid in the duplicate | |
597 | * @return int id of the new instance | |
598 | */ | |
599 | public function copy($raterid, $itemid) { | |
600 | global $DB; | |
601 | $instanceid = parent::copy($raterid, $itemid); | |
602 | $currentgrade = $this->get_rubric_filling(); | |
5060997b MG |
603 | foreach ($currentgrade['criteria'] as $criterionid => $record) { |
604 | $params = array('forminstanceid' => $instanceid, 'criterionid' => $criterionid, | |
605 | 'levelid' => $record['levelid'], 'remark' => $record['remark'], 'remarkformat' => $record['remarkformat']); | |
36937f02 MG |
606 | $DB->insert_record('gradingform_rubric_fillings', $params); |
607 | } | |
36937f02 MG |
608 | return $instanceid; |
609 | } | |
610 | ||
611 | /** | |
612 | * Validates that rubric is fully completed and contains valid grade on each criterion | |
613 | * @return boolean true if the form data is validated and contains no errors | |
614 | */ | |
615 | public function validate_grading_element($elementvalue) { | |
36937f02 | 616 | $criteria = $this->get_controller()->get_definition()->rubric_criteria; |
5060997b | 617 | if (!isset($elementvalue['criteria']) || !is_array($elementvalue['criteria']) || sizeof($elementvalue['criteria']) < sizeof($criteria)) { |
36937f02 MG |
618 | return false; |
619 | } | |
620 | foreach ($criteria as $id => $criterion) { | |
5060997b MG |
621 | if (!isset($elementvalue['criteria'][$id]['levelid']) |
622 | || !array_key_exists($elementvalue['criteria'][$id]['levelid'], $criterion['levels'])) { | |
36937f02 MG |
623 | return false; |
624 | } | |
625 | } | |
626 | return true; | |
627 | } | |
628 | ||
629 | /** | |
630 | * Retrieves from DB and returns the data how this rubric was filled | |
631 | * | |
5060997b | 632 | * @param boolean $force whether to force DB query even if the data is cached |
36937f02 MG |
633 | * @return array |
634 | */ | |
5060997b | 635 | public function get_rubric_filling($force = false) { |
36937f02 | 636 | global $DB; |
5060997b MG |
637 | if ($this->rubric === null || $force) { |
638 | $records = $DB->get_records('gradingform_rubric_fillings', array('forminstanceid' => $this->get_id())); | |
639 | $this->rubric = array('criteria' => array()); | |
640 | foreach ($records as $record) { | |
641 | $this->rubric['criteria'][$record->criterionid] = (array)$record; | |
36937f02 | 642 | } |
36937f02 | 643 | } |
5060997b | 644 | return $this->rubric; |
36937f02 MG |
645 | } |
646 | ||
647 | /** | |
648 | * Updates the instance with the data received from grading form. This function may be | |
649 | * called via AJAX when grading is not yet completed, so it does not change the | |
650 | * status of the instance. | |
fc05f222 MG |
651 | * |
652 | * @param array $data | |
36937f02 MG |
653 | */ |
654 | public function update($data) { | |
655 | global $DB; | |
656 | $currentgrade = $this->get_rubric_filling(); | |
fc05f222 | 657 | parent::update($data); |
5060997b MG |
658 | foreach ($data['criteria'] as $criterionid => $record) { |
659 | if (!array_key_exists($criterionid, $currentgrade['criteria'])) { | |
660 | $newrecord = array('forminstanceid' => $this->get_id(), 'criterionid' => $criterionid, | |
0136124e MG |
661 | 'levelid' => $record['levelid'], 'remarkformat' => FORMAT_MOODLE); |
662 | if (isset($record['remark'])) { | |
663 | $newrecord['remark'] = $record['remark']; | |
664 | } | |
5060997b MG |
665 | $DB->insert_record('gradingform_rubric_fillings', $newrecord); |
666 | } else { | |
667 | $newrecord = array('id' => $currentgrade['criteria'][$criterionid]['id']); | |
668 | foreach (array('levelid', 'remark'/*, 'remarkformat' TODO */) as $key) { | |
0136124e | 669 | if (isset($record[$key]) && $currentgrade['criteria'][$criterionid][$key] != $record[$key]) { |
5060997b MG |
670 | $newrecord[$key] = $record[$key]; |
671 | } | |
672 | } | |
673 | if (count($newrecord) > 1) { | |
674 | $DB->update_record('gradingform_rubric_fillings', $newrecord); | |
675 | } | |
36937f02 MG |
676 | } |
677 | } | |
5060997b MG |
678 | foreach ($currentgrade['criteria'] as $criterionid => $record) { |
679 | if (!array_key_exists($criterionid, $data['criteria'])) { | |
680 | $DB->delete_records('gradingform_rubric_fillings', array('id' => $record['id'])); | |
36937f02 MG |
681 | } |
682 | } | |
5060997b | 683 | $this->get_rubric_filling(true); |
36937f02 MG |
684 | } |
685 | ||
686 | /** | |
687 | * Calculates the grade to be pushed to the gradebook | |
9e2eca0f MG |
688 | * |
689 | * @return int the valid grade from $this->get_controller()->get_grade_range() | |
36937f02 MG |
690 | */ |
691 | public function get_grade() { | |
692 | global $DB, $USER; | |
693 | $grade = $this->get_rubric_filling(); | |
694 | ||
695 | $minscore = 0; | |
696 | $maxscore = 0; | |
697 | foreach ($this->get_controller()->get_definition()->rubric_criteria as $id => $criterion) { | |
3599b113 MG |
698 | $scores = array(); |
699 | foreach ($criterion['levels'] as $level) { | |
700 | $scores[] = $level['score']; | |
701 | } | |
702 | sort($scores); | |
6184b843 MG |
703 | $minscore += $scores[0]; |
704 | $maxscore += $scores[sizeof($scores)-1]; | |
36937f02 MG |
705 | } |
706 | ||
9e2eca0f MG |
707 | if ($maxscore <= $minscore) { |
708 | return -1; | |
709 | } | |
710 | ||
711 | $graderange = array_keys($this->get_controller()->get_grade_range()); | |
712 | if (empty($graderange)) { | |
36937f02 MG |
713 | return -1; |
714 | } | |
9e2eca0f MG |
715 | sort($graderange); |
716 | $mingrade = $graderange[0]; | |
717 | $maxgrade = $graderange[sizeof($graderange) - 1]; | |
36937f02 MG |
718 | |
719 | $curscore = 0; | |
5060997b MG |
720 | foreach ($grade['criteria'] as $id => $record) { |
721 | $curscore += $this->get_controller()->get_definition()->rubric_criteria[$id]['levels'][$record['levelid']]['score']; | |
36937f02 | 722 | } |
9e2eca0f | 723 | return round(($curscore-$minscore)/($maxscore-$minscore)*($maxgrade-$mingrade), 0) + $mingrade; // TODO mapping |
36937f02 MG |
724 | } |
725 | ||
36937f02 MG |
726 | /** |
727 | * Returns html for form element of type 'grading'. | |
728 | * | |
729 | * @param moodle_page $page | |
730 | * @param MoodleQuickForm_grading $formelement | |
731 | * @return string | |
732 | */ | |
733 | public function render_grading_element($page, $gradingformelement) { | |
734 | global $USER; | |
735 | if (!$gradingformelement->_flagFrozen) { | |
736 | $module = array('name'=>'gradingform_rubric', 'fullpath'=>'/grade/grading/form/rubric/js/rubric.js'); | |
737 | $page->requires->js_init_call('M.gradingform_rubric.init', array(array('name' => $gradingformelement->getName())), true, $module); | |
738 | $mode = gradingform_rubric_controller::DISPLAY_EVAL; | |
739 | } else { | |
740 | if ($gradingformelement->_persistantFreeze) { | |
741 | $mode = gradingform_rubric_controller::DISPLAY_EVAL_FROZEN; | |
742 | } else { | |
743 | $mode = gradingform_rubric_controller::DISPLAY_REVIEW; | |
744 | } | |
745 | } | |
746 | $criteria = $this->get_controller()->get_definition()->rubric_criteria; | |
39c6f4b6 | 747 | $options = $this->get_controller()->get_options(); |
36937f02 | 748 | $value = $gradingformelement->getValue(); |
0136124e | 749 | $html = ''; |
36937f02 MG |
750 | if ($value === null) { |
751 | $value = $this->get_rubric_filling(); | |
0136124e MG |
752 | } else if (!$this->validate_grading_element($value)) { |
753 | $html .= html_writer::tag('div', get_string('rubricnotcompleted', 'gradingform_rubric'), array('class' => 'gradingform_rubric-error')); | |
754 | } | |
755 | $currentinstance = $this->get_current_instance(); | |
756 | if ($currentinstance && $currentinstance->get_status() == gradingform_instance::INSTANCE_STATUS_NEEDUPDATE) { | |
757 | $html .= html_writer::tag('div', get_string('needregrademessage', 'gradingform_rubric'), array('class' => 'gradingform_rubric-regrade')); | |
36937f02 | 758 | } |
188e840b MG |
759 | $haschanges = false; |
760 | if ($currentinstance) { | |
761 | $curfilling = $currentinstance->get_rubric_filling(); | |
762 | foreach ($curfilling['criteria'] as $criterionid => $curvalues) { | |
763 | $value['criteria'][$criterionid]['savedlevelid'] = $curvalues['levelid']; | |
764 | $newremark = null; | |
765 | $newlevelid = null; | |
766 | if (isset($value['criteria'][$criterionid]['remark'])) $newremark = $value['criteria'][$criterionid]['remark']; | |
767 | if (isset($value['criteria'][$criterionid]['levelid'])) $newlevelid = $value['criteria'][$criterionid]['levelid']; | |
768 | if ($newlevelid != $curvalues['levelid'] || $newremark != $curvalues['remark']) { | |
769 | $haschanges = true; | |
770 | } | |
771 | } | |
772 | } | |
773 | if ($this->get_data('isrestored') && $haschanges) { | |
774 | $html .= html_writer::tag('div', get_string('restoredfromdraft', 'gradingform_rubric'), array('class' => 'gradingform_rubric-restored')); | |
775 | } | |
0136124e MG |
776 | $html .= $this->get_controller()->get_renderer($page)->display_rubric($criteria, $options, $mode, $gradingformelement->getName(), $value); |
777 | return $html; | |
36937f02 | 778 | } |
3599b113 | 779 | } |