Commit | Line | Data |
---|---|---|
77143217 DM |
1 | <?php |
2 | // This file is part of Moodle - http://moodle.org/ | |
3 | // | |
4 | // Moodle is free software: you can redistribute it and/or modify | |
5 | // it under the terms of the GNU General Public License as published by | |
6 | // the Free Software Foundation, either version 3 of the License, or | |
7 | // (at your option) any later version. | |
8 | // | |
9 | // Moodle is distributed in the hope that it will be useful, | |
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of | |
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
12 | // GNU General Public License for more details. | |
13 | // | |
14 | // You should have received a copy of the GNU General Public License | |
15 | // along with Moodle. If not, see <http://www.gnu.org/licenses/>. | |
16 | ||
17 | /** | |
18 | * Grading method controller for the guide plugin | |
19 | * | |
20 | * @package gradingform_guide | |
21 | * @copyright 2012 Dan Marsden <dan@danmarsden.com> | |
22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
23 | */ | |
24 | ||
25 | defined('MOODLE_INTERNAL') || die(); | |
26 | ||
27 | require_once($CFG->dirroot.'/grade/grading/form/lib.php'); | |
28 | ||
29 | /** | |
30 | * This controller encapsulates the guide grading logic | |
31 | * | |
32 | * @package gradingform_guide | |
33 | * @copyright 2012 Dan Marsden <dan@danmarsden.com> | |
34 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
35 | */ | |
36 | class gradingform_guide_controller extends gradingform_controller { | |
37 | // Modes of displaying the guide (used in gradingform_guide_renderer). | |
38 | /** guide display mode: For editing (moderator or teacher creates a guide) */ | |
39 | const DISPLAY_EDIT_FULL = 1; | |
40 | /** guide display mode: Preview the guide design with hidden fields */ | |
41 | const DISPLAY_EDIT_FROZEN = 2; | |
42 | /** guide display mode: Preview the guide design (for person with manage permission) */ | |
43 | const DISPLAY_PREVIEW = 3; | |
44 | /** guide display mode: Preview the guide (for people being graded) */ | |
45 | const DISPLAY_PREVIEW_GRADED= 8; | |
46 | /** guide display mode: For evaluation, enabled (teacher grades a student) */ | |
47 | const DISPLAY_EVAL = 4; | |
48 | /** guide display mode: For evaluation, with hidden fields */ | |
49 | const DISPLAY_EVAL_FROZEN = 5; | |
50 | /** guide display mode: Teacher reviews filled guide */ | |
51 | const DISPLAY_REVIEW = 6; | |
52 | /** guide display mode: Dispaly filled guide (i.e. students see their grades) */ | |
53 | const DISPLAY_VIEW = 7; | |
54 | ||
55 | /** @var stdClass|false the definition structure */ | |
56 | protected $moduleinstance = false; | |
57 | ||
58 | /** | |
59 | * Extends the module settings navigation with the guide grading settings | |
60 | * | |
61 | * This function is called when the context for the page is an activity module with the | |
62 | * FEATURE_ADVANCED_GRADING, the user has the permission moodle/grade:managegradingforms | |
63 | * and there is an area with the active grading method set to 'guide'. | |
64 | * | |
65 | * @param settings_navigation $settingsnav {@link settings_navigation} | |
66 | * @param navigation_node $node {@link navigation_node} | |
67 | */ | |
68 | public function extend_settings_navigation(settings_navigation $settingsnav, navigation_node $node=null) { | |
69 | $node->add(get_string('definemarkingguide', 'gradingform_guide'), | |
70 | $this->get_editor_url(), settings_navigation::TYPE_CUSTOM, | |
71 | null, null, new pix_icon('icon', '', 'gradingform_guide')); | |
72 | } | |
73 | ||
74 | /** | |
75 | * Extends the module navigation | |
76 | * | |
77 | * This function is called when the context for the page is an activity module with the | |
78 | * FEATURE_ADVANCED_GRADING and there is an area with the active grading method set to the given plugin. | |
79 | * | |
80 | * @param global_navigation $navigation {@link global_navigation} | |
81 | * @param navigation_node $node {@link navigation_node} | |
82 | * @return void | |
83 | */ | |
84 | public function extend_navigation(global_navigation $navigation, navigation_node $node=null) { | |
85 | if (has_capability('moodle/grade:managegradingforms', $this->get_context())) { | |
86 | // No need for preview if user can manage forms, he will have link to manage.php in settings instead. | |
87 | return; | |
88 | } | |
89 | if ($this->is_form_defined() && ($options = $this->get_options()) && !empty($options['alwaysshowdefinition'])) { | |
90 | $node->add(get_string('gradingof', 'gradingform_guide', get_grading_manager($this->get_areaid())->get_area_title()), | |
91 | new moodle_url('/grade/grading/form/'.$this->get_method_name().'/preview.php', | |
92 | array('areaid' => $this->get_areaid())), settings_navigation::TYPE_CUSTOM); | |
93 | } | |
94 | } | |
95 | ||
96 | /** | |
97 | * Saves the guide definition into the database | |
98 | * | |
99 | * @see parent::update_definition() | |
100 | * @param stdClass $newdefinition guide definition data as coming from gradingform_guide_editguide::get_data() | |
101 | * @param int $usermodified optional userid of the author of the definition, defaults to the current user | |
102 | */ | |
103 | public function update_definition(stdClass $newdefinition, $usermodified = null) { | |
104 | $this->update_or_check_guide($newdefinition, $usermodified, true); | |
105 | if (isset($newdefinition->guide['regrade']) && $newdefinition->guide['regrade']) { | |
106 | $this->mark_for_regrade(); | |
107 | } | |
108 | } | |
109 | ||
110 | /** | |
111 | * Either saves the guide definition into the database or check if it has been changed. | |
112 | * | |
113 | * Returns the level of changes: | |
114 | * 0 - no changes | |
115 | * 1 - only texts or criteria sortorders are changed, students probably do not require re-grading | |
116 | * 2 - added levels but maximum score on guide is the same, students still may not require re-grading | |
117 | * 3 - removed criteria or changed number of points, students require re-grading but may be re-graded automatically | |
118 | * 4 - removed levels - students require re-grading and not all students may be re-graded automatically | |
119 | * 5 - added criteria - all students require manual re-grading | |
120 | * | |
121 | * @param stdClass $newdefinition guide definition data as coming from gradingform_guide_editguide::get_data() | |
122 | * @param int|null $usermodified optional userid of the author of the definition, defaults to the current user | |
123 | * @param bool $doupdate if true actually updates DB, otherwise performs a check | |
124 | * @return int | |
125 | */ | |
126 | public function update_or_check_guide(stdClass $newdefinition, $usermodified = null, $doupdate = false) { | |
127 | global $DB; | |
128 | ||
129 | // Firstly update the common definition data in the {grading_definition} table. | |
130 | if ($this->definition === false) { | |
131 | if (!$doupdate) { | |
132 | // If we create the new definition there is no such thing as re-grading anyway. | |
133 | return 5; | |
134 | } | |
135 | // If definition does not exist yet, create a blank one | |
136 | // (we need id to save files embedded in description). | |
137 | parent::update_definition(new stdClass(), $usermodified); | |
138 | parent::load_definition(); | |
139 | } | |
140 | if (!isset($newdefinition->guide['options'])) { | |
141 | $newdefinition->guide['options'] = self::get_default_options(); | |
142 | } | |
143 | $newdefinition->options = json_encode($newdefinition->guide['options']); | |
144 | $editoroptions = self::description_form_field_options($this->get_context()); | |
145 | $newdefinition = file_postupdate_standard_editor($newdefinition, 'description', $editoroptions, $this->get_context(), | |
146 | 'grading', 'description', $this->definition->id); | |
147 | ||
148 | // Reload the definition from the database. | |
149 | $currentdefinition = $this->get_definition(true); | |
150 | ||
151 | // Update guide data. | |
152 | $haschanges = array(); | |
153 | if (empty($newdefinition->guide['criteria'])) { | |
154 | $newcriteria = array(); | |
155 | } else { | |
156 | $newcriteria = $newdefinition->guide['criteria']; // New ones to be saved. | |
157 | } | |
158 | $currentcriteria = $currentdefinition->guide_criteria; | |
159 | $criteriafields = array('sortorder', 'description', 'descriptionformat', 'descriptionmarkers', | |
160 | 'descriptionmarkersformat', 'shortname', 'maxscore'); | |
161 | foreach ($newcriteria as $id => $criterion) { | |
162 | if (preg_match('/^NEWID\d+$/', $id)) { | |
163 | // Insert criterion into DB. | |
164 | $data = array('definitionid' => $this->definition->id, 'descriptionformat' => FORMAT_MOODLE, | |
165 | 'descriptionmarkersformat' => FORMAT_MOODLE); // TODO format is not supported yet. | |
166 | foreach ($criteriafields as $key) { | |
167 | if (array_key_exists($key, $criterion)) { | |
168 | $data[$key] = $criterion[$key]; | |
169 | } | |
170 | } | |
171 | if ($doupdate) { | |
172 | $id = $DB->insert_record('gradingform_guide_criteria', $data); | |
173 | } | |
174 | $haschanges[5] = true; | |
175 | } else { | |
176 | // Update criterion in DB. | |
177 | $data = array(); | |
178 | foreach ($criteriafields as $key) { | |
179 | if (array_key_exists($key, $criterion) && $criterion[$key] != $currentcriteria[$id][$key]) { | |
180 | $data[$key] = $criterion[$key]; | |
181 | } | |
182 | } | |
183 | if (!empty($data)) { | |
184 | // Update only if something is changed. | |
185 | $data['id'] = $id; | |
186 | if ($doupdate) { | |
187 | $DB->update_record('gradingform_guide_criteria', $data); | |
188 | } | |
189 | $haschanges[1] = true; | |
190 | } | |
191 | } | |
192 | } | |
193 | // Remove deleted criteria from DB. | |
194 | foreach (array_keys($currentcriteria) as $id) { | |
195 | if (!array_key_exists($id, $newcriteria)) { | |
196 | if ($doupdate) { | |
197 | $DB->delete_records('gradingform_guide_criteria', array('id' => $id)); | |
198 | } | |
199 | $haschanges[3] = true; | |
200 | } | |
201 | } | |
202 | // Now handle comments. | |
203 | if (empty($newdefinition->guide['comments'])) { | |
204 | $newcomment = array(); | |
205 | } else { | |
206 | $newcomment = $newdefinition->guide['comments']; // New ones to be saved. | |
207 | } | |
208 | $currentcomments = $currentdefinition->guide_comment; | |
209 | $commentfields = array('sortorder', 'description'); | |
210 | foreach ($newcomment as $id => $comment) { | |
211 | if (preg_match('/^NEWID\d+$/', $id)) { | |
212 | // Insert criterion into DB. | |
213 | $data = array('definitionid' => $this->definition->id, 'descriptionformat' => FORMAT_MOODLE); | |
214 | foreach ($commentfields as $key) { | |
215 | if (array_key_exists($key, $comment)) { | |
216 | $data[$key] = $comment[$key]; | |
217 | } | |
218 | } | |
219 | if ($doupdate) { | |
220 | $id = $DB->insert_record('gradingform_guide_comments', $data); | |
221 | } | |
222 | } else { | |
223 | // Update criterion in DB. | |
224 | $data = array(); | |
225 | foreach ($commentfields as $key) { | |
226 | if (array_key_exists($key, $comment) && $comment[$key] != $currentcomments[$id][$key]) { | |
227 | $data[$key] = $comment[$key]; | |
228 | } | |
229 | } | |
230 | if (!empty($data)) { | |
231 | // Update only if something is changed. | |
232 | $data['id'] = $id; | |
233 | if ($doupdate) { | |
234 | $DB->update_record('gradingform_guide_comments', $data); | |
235 | } | |
236 | } | |
237 | } | |
238 | } | |
239 | // Remove deleted criteria from DB. | |
240 | foreach (array_keys($currentcomments) as $id) { | |
241 | if (!array_key_exists($id, $newcomment)) { | |
242 | if ($doupdate) { | |
243 | $DB->delete_records('gradingform_guide_comments', array('id' => $id)); | |
244 | } | |
245 | } | |
246 | } | |
247 | // End comments handle. | |
248 | foreach (array('status', 'description', 'descriptionformat', 'name', 'options') as $key) { | |
249 | if (isset($newdefinition->$key) && $newdefinition->$key != $this->definition->$key) { | |
250 | $haschanges[1] = true; | |
251 | } | |
252 | } | |
253 | if ($usermodified && $usermodified != $this->definition->usermodified) { | |
254 | $haschanges[1] = true; | |
255 | } | |
256 | if (!count($haschanges)) { | |
257 | return 0; | |
258 | } | |
259 | if ($doupdate) { | |
260 | parent::update_definition($newdefinition, $usermodified); | |
261 | $this->load_definition(); | |
262 | } | |
263 | // Return the maximum level of changes. | |
264 | $changelevels = array_keys($haschanges); | |
265 | sort($changelevels); | |
266 | return array_pop($changelevels); | |
267 | } | |
268 | ||
269 | /** | |
270 | * Marks all instances filled with this guide with the status INSTANCE_STATUS_NEEDUPDATE | |
271 | */ | |
272 | public function mark_for_regrade() { | |
273 | global $DB; | |
274 | if ($this->has_active_instances()) { | |
275 | $conditions = array('definitionid' => $this->definition->id, | |
276 | 'status' => gradingform_instance::INSTANCE_STATUS_ACTIVE); | |
277 | $DB->set_field('grading_instances', 'status', gradingform_instance::INSTANCE_STATUS_NEEDUPDATE, $conditions); | |
278 | } | |
279 | } | |
280 | ||
281 | /** | |
282 | * Loads the guide form definition if it exists | |
283 | * | |
284 | * There is a new array called 'guide_criteria' appended to the list of parent's definition properties. | |
285 | */ | |
286 | protected function load_definition() { | |
287 | global $DB; | |
288 | ||
289 | // Check to see if the user prefs have changed - putting here as this function is called on post even when | |
290 | // validation on the page fails. - hard to find a better place to locate this as it is specific to the guide. | |
291 | $showdesc = optional_param('showmarkerdesc', null, PARAM_BOOL); // Check if we need to change pref. | |
292 | $showdescstudent = optional_param('showstudentdesc', null, PARAM_BOOL); // Check if we need to change pref. | |
293 | if ($showdesc !== null) { | |
294 | set_user_preference('gradingform_guide-showmarkerdesc', $showdesc); | |
295 | } | |
296 | if ($showdescstudent !== null) { | |
297 | set_user_preference('gradingform_guide-showstudentdesc', $showdescstudent); | |
298 | } | |
299 | ||
300 | // Get definition. | |
301 | $definition = $DB->get_record('grading_definitions', array('areaid' => $this->areaid, | |
302 | 'method' => $this->get_method_name()), '*'); | |
303 | if (!$definition) { | |
304 | // The definition doesn't have to exist. It may be that we are only now creating it. | |
305 | $this->definition = false; | |
306 | return false; | |
307 | } | |
308 | ||
309 | $this->definition = $definition; | |
310 | // Now get criteria. | |
311 | $this->definition->guide_criteria = array(); | |
312 | $this->definition->guide_comment = array(); | |
313 | $criteria = $DB->get_recordset('gradingform_guide_criteria', array('definitionid' => $this->definition->id), 'sortorder'); | |
314 | foreach ($criteria as $criterion) { | |
315 | foreach (array('id', 'sortorder', 'description', 'descriptionformat', | |
316 | 'maxscore', 'descriptionmarkers', 'descriptionmarkersformat', 'shortname') as $fieldname) { | |
317 | if ($fieldname == 'maxscore') { // Strip any trailing 0. | |
318 | $this->definition->guide_criteria[$criterion->id][$fieldname] = (float)$criterion->{$fieldname}; | |
319 | } else { | |
320 | $this->definition->guide_criteria[$criterion->id][$fieldname] = $criterion->{$fieldname}; | |
321 | } | |
322 | } | |
323 | } | |
324 | $criteria->close(); | |
325 | ||
326 | // Now get comments. | |
327 | $comments = $DB->get_recordset('gradingform_guide_comments', array('definitionid' => $this->definition->id), 'sortorder'); | |
328 | foreach ($comments as $comment) { | |
329 | foreach (array('id', 'sortorder', 'description', 'descriptionformat') as $fieldname) { | |
330 | $this->definition->guide_comment[$comment->id][$fieldname] = $comment->{$fieldname}; | |
331 | } | |
332 | } | |
333 | $comments->close(); | |
77143217 DM |
334 | if (empty($this->moduleinstance)) { // Only set if empty. |
335 | $modulename = $this->get_component(); | |
336 | $context = $this->get_context(); | |
337 | if (strpos($modulename, 'mod_') === 0) { | |
338 | $dbman = $DB->get_manager(); | |
339 | $modulename = substr($modulename, 4); | |
340 | if ($dbman->table_exists($modulename)) { | |
9fdbd69e | 341 | $cm = get_coursemodule_from_id('', $context->instanceid); |
b4b1d490 | 342 | $this->moduleinstance = $DB->get_record($modulename, array("id"=>$cm->instance)); |
77143217 DM |
343 | } |
344 | } | |
345 | } | |
346 | } | |
347 | ||
348 | /** | |
349 | * Returns the default options for the guide display | |
350 | * | |
351 | * @return array | |
352 | */ | |
353 | public static function get_default_options() { | |
354 | $options = array( | |
355 | 'alwaysshowdefinition' => 1, | |
356 | 'showmarkspercriterionstudents' => 1, | |
357 | ); | |
358 | return $options; | |
359 | } | |
360 | ||
361 | /** | |
362 | * Gets the options of this guide definition, fills the missing options with default values | |
363 | * | |
364 | * @return array | |
365 | */ | |
366 | public function get_options() { | |
367 | $options = self::get_default_options(); | |
368 | if (!empty($this->definition->options)) { | |
369 | $thisoptions = json_decode($this->definition->options); | |
370 | foreach ($thisoptions as $option => $value) { | |
371 | $options[$option] = $value; | |
372 | } | |
373 | } | |
374 | return $options; | |
375 | } | |
376 | ||
377 | /** | |
378 | * Converts the current definition into an object suitable for the editor form's set_data() | |
379 | * | |
380 | * @param bool $addemptycriterion whether to add an empty criterion if the guide is completely empty (just being created) | |
381 | * @return stdClass | |
382 | */ | |
383 | public function get_definition_for_editing($addemptycriterion = false) { | |
384 | ||
385 | $definition = $this->get_definition(); | |
386 | $properties = new stdClass(); | |
387 | $properties->areaid = $this->areaid; | |
388 | if (isset($this->moduleinstance->grade)) { | |
389 | $properties->modulegrade = $this->moduleinstance->grade; | |
390 | } | |
391 | if ($definition) { | |
392 | foreach (array('id', 'name', 'description', 'descriptionformat', 'status') as $key) { | |
393 | $properties->$key = $definition->$key; | |
394 | } | |
395 | $options = self::description_form_field_options($this->get_context()); | |
396 | $properties = file_prepare_standard_editor($properties, 'description', $options, $this->get_context(), | |
397 | 'grading', 'description', $definition->id); | |
398 | } | |
399 | $properties->guide = array('criteria' => array(), 'options' => $this->get_options(), 'comments' => array()); | |
400 | if (!empty($definition->guide_criteria)) { | |
401 | $properties->guide['criteria'] = $definition->guide_criteria; | |
402 | } else if (!$definition && $addemptycriterion) { | |
403 | $properties->guide['criteria'] = array('addcriterion' => 1); | |
404 | } | |
405 | if (!empty($definition->guide_comment)) { | |
406 | $properties->guide['comments'] = $definition->guide_comment; | |
407 | } else if (!$definition && $addemptycriterion) { | |
408 | $properties->guide['comments'] = array('addcomment' => 1); | |
409 | } | |
410 | return $properties; | |
411 | } | |
412 | ||
413 | /** | |
414 | * Returns the form definition suitable for cloning into another area | |
415 | * | |
416 | * @see parent::get_definition_copy() | |
417 | * @param gradingform_controller $target the controller of the new copy | |
418 | * @return stdClass definition structure to pass to the target's {@link update_definition()} | |
419 | */ | |
420 | public function get_definition_copy(gradingform_controller $target) { | |
421 | ||
422 | $new = parent::get_definition_copy($target); | |
423 | $old = $this->get_definition_for_editing(); | |
424 | $new->description_editor = $old->description_editor; | |
425 | $new->guide = array('criteria' => array(), 'options' => $old->guide['options'], 'comments' => array()); | |
426 | $newcritid = 1; | |
427 | foreach ($old->guide['criteria'] as $oldcritid => $oldcrit) { | |
428 | unset($oldcrit['id']); | |
429 | $new->guide['criteria']['NEWID'.$newcritid] = $oldcrit; | |
430 | $newcritid++; | |
431 | } | |
432 | $newcomid = 1; | |
433 | foreach ($old->guide['comments'] as $oldcritid => $oldcom) { | |
434 | unset($oldcom['id']); | |
435 | $new->guide['comments']['NEWID'.$newcomid] = $oldcom; | |
436 | $newcomid++; | |
437 | } | |
438 | return $new; | |
439 | } | |
440 | ||
441 | /** | |
442 | * Options for displaying the guide description field in the form | |
443 | * | |
444 | * @param context $context | |
445 | * @return array options for the form description field | |
446 | */ | |
447 | public static function description_form_field_options($context) { | |
448 | global $CFG; | |
449 | return array( | |
450 | 'maxfiles' => -1, | |
451 | 'maxbytes' => get_max_upload_file_size($CFG->maxbytes), | |
452 | 'context' => $context, | |
453 | ); | |
454 | } | |
455 | ||
456 | /** | |
457 | * Formats the definition description for display on page | |
458 | * | |
459 | * @return string | |
460 | */ | |
461 | public function get_formatted_description() { | |
462 | if (!isset($this->definition->description)) { | |
463 | return ''; | |
464 | } | |
465 | $context = $this->get_context(); | |
466 | ||
467 | $options = self::description_form_field_options($this->get_context()); | |
468 | $description = file_rewrite_pluginfile_urls($this->definition->description, 'pluginfile.php', $context->id, | |
469 | 'grading', 'description', $this->definition->id, $options); | |
470 | ||
471 | $formatoptions = array( | |
472 | 'noclean' => false, | |
473 | 'trusted' => false, | |
474 | 'filter' => true, | |
475 | 'context' => $context | |
476 | ); | |
477 | return format_text($description, $this->definition->descriptionformat, $formatoptions); | |
478 | } | |
479 | ||
480 | /** | |
481 | * Returns the guide plugin renderer | |
482 | * | |
483 | * @param moodle_page $page the target page | |
484 | * @return gradingform_guide_renderer | |
485 | */ | |
486 | public function get_renderer(moodle_page $page) { | |
487 | return $page->get_renderer('gradingform_'. $this->get_method_name()); | |
488 | } | |
489 | ||
490 | /** | |
491 | * Returns the HTML code displaying the preview of the grading form | |
492 | * | |
493 | * @param moodle_page $page the target page | |
494 | * @return string | |
495 | */ | |
496 | public function render_preview(moodle_page $page) { | |
497 | ||
498 | if (!$this->is_form_defined()) { | |
499 | throw new coding_exception('It is the caller\'s responsibility to make sure that the form is actually defined'); | |
500 | } | |
501 | ||
502 | $output = $this->get_renderer($page); | |
503 | $criteria = $this->definition->guide_criteria; | |
504 | $comments = $this->definition->guide_comment; | |
505 | $options = $this->get_options(); | |
506 | $guide = ''; | |
507 | if (has_capability('moodle/grade:managegradingforms', $page->context)) { | |
508 | $guide .= $output->display_guide_mapping_explained($this->get_min_max_score()); | |
509 | $guide .= $output->display_guide($criteria, $comments, $options, self::DISPLAY_PREVIEW, 'guide'); | |
510 | } else { | |
511 | $guide .= $output->display_guide($criteria, $comments, $options, self::DISPLAY_PREVIEW_GRADED, 'guide'); | |
512 | } | |
513 | ||
514 | return $guide; | |
515 | } | |
516 | ||
517 | /** | |
518 | * Deletes the guide definition and all the associated information | |
519 | */ | |
520 | protected function delete_plugin_definition() { | |
521 | global $DB; | |
522 | ||
523 | // Get the list of instances. | |
524 | $instances = array_keys($DB->get_records('grading_instances', array('definitionid' => $this->definition->id), '', 'id')); | |
525 | // Delete all fillings. | |
526 | $DB->delete_records_list('gradingform_guide_fillings', 'instanceid', $instances); | |
527 | // Delete instances. | |
528 | $DB->delete_records_list('grading_instances', 'id', $instances); | |
529 | // Get the list of criteria records. | |
530 | $criteria = array_keys($DB->get_records('gradingform_guide_criteria', | |
531 | array('definitionid' => $this->definition->id), '', 'id')); | |
532 | // Delete critera. | |
533 | $DB->delete_records_list('gradingform_guide_criteria', 'id', $criteria); | |
534 | // Delete comments. | |
535 | $DB->delete_records('gradingform_guide_comments', array('definitionid' => $this->definition->id)); | |
536 | } | |
537 | ||
538 | /** | |
539 | * If instanceid is specified and grading instance exists and it is created by this rater for | |
540 | * this item, this instance is returned. | |
541 | * If there exists a draft for this raterid+itemid, take this draft (this is the change from parent) | |
542 | * Otherwise new instance is created for the specified rater and itemid | |
543 | * | |
544 | * @param int $instanceid | |
545 | * @param int $raterid | |
546 | * @param int $itemid | |
547 | * @return gradingform_instance | |
548 | */ | |
549 | public function get_or_create_instance($instanceid, $raterid, $itemid) { | |
550 | global $DB; | |
551 | if ($instanceid && | |
552 | $instance = $DB->get_record('grading_instances', | |
553 | array('id' => $instanceid, 'raterid' => $raterid, 'itemid' => $itemid), '*', IGNORE_MISSING)) { | |
554 | return $this->get_instance($instance); | |
555 | } | |
556 | if ($itemid && $raterid) { | |
557 | if ($rs = $DB->get_records('grading_instances', array('raterid' => $raterid, 'itemid' => $itemid), | |
558 | 'timemodified DESC', '*', 0, 1)) { | |
559 | $record = reset($rs); | |
560 | $currentinstance = $this->get_current_instance($raterid, $itemid); | |
561 | if ($record->status == gradingform_guide_instance::INSTANCE_STATUS_INCOMPLETE && | |
562 | (!$currentinstance || $record->timemodified > $currentinstance->get_data('timemodified'))) { | |
563 | $record->isrestored = true; | |
564 | return $this->get_instance($record); | |
565 | } | |
566 | } | |
567 | } | |
568 | return $this->create_instance($raterid, $itemid); | |
569 | } | |
570 | ||
571 | /** | |
572 | * Returns html code to be included in student's feedback. | |
573 | * | |
574 | * @param moodle_page $page | |
575 | * @param int $itemid | |
576 | * @param array $gradinginfo result of function grade_get_grades | |
577 | * @param string $defaultcontent default string to be returned if no active grading is found | |
578 | * @param bool $cangrade whether current user has capability to grade in this context | |
579 | * @return string | |
580 | */ | |
581 | public function render_grade($page, $itemid, $gradinginfo, $defaultcontent, $cangrade) { | |
582 | return $this->get_renderer($page)->display_instances($this->get_active_instances($itemid), $defaultcontent, $cangrade); | |
583 | } | |
584 | ||
585 | // Full-text search support. | |
586 | ||
587 | /** | |
588 | * Prepare the part of the search query to append to the FROM statement | |
589 | * | |
590 | * @param string $gdid the alias of grading_definitions.id column used by the caller | |
591 | * @return string | |
592 | */ | |
593 | public static function sql_search_from_tables($gdid) { | |
594 | return " LEFT JOIN {gradingform_guide_criteria} gc ON (gc.definitionid = $gdid)"; | |
595 | } | |
596 | ||
597 | /** | |
598 | * Prepare the parts of the SQL WHERE statement to search for the given token | |
599 | * | |
600 | * The returned array cosists of the list of SQL comparions and the list of | |
601 | * respective parameters for the comparisons. The returned chunks will be joined | |
602 | * with other conditions using the OR operator. | |
603 | * | |
604 | * @param string $token token to search for | |
605 | * @return array An array containing two more arrays | |
606 | * Array of search SQL fragments | |
607 | * Array of params for the search fragments | |
608 | */ | |
609 | public static function sql_search_where($token) { | |
610 | global $DB; | |
611 | ||
612 | $subsql = array(); | |
613 | $params = array(); | |
614 | ||
615 | // Search in guide criteria description. | |
616 | $subsql[] = $DB->sql_like('gc.description', '?', false, false); | |
617 | $params[] = '%'.$DB->sql_like_escape($token).'%'; | |
618 | ||
619 | return array($subsql, $params); | |
620 | } | |
621 | ||
622 | /** | |
623 | * Calculates and returns the possible minimum and maximum score (in points) for this guide | |
624 | * | |
625 | * @return array | |
626 | */ | |
627 | public function get_min_max_score() { | |
628 | if (!$this->is_form_available()) { | |
629 | return null; | |
630 | } | |
631 | $returnvalue = array('minscore' => 0, 'maxscore' => 0); | |
632 | $maxscore = 0; | |
633 | foreach ($this->get_definition()->guide_criteria as $id => $criterion) { | |
634 | $maxscore += $criterion['maxscore']; | |
635 | } | |
636 | $returnvalue['maxscore'] = $maxscore; | |
637 | $returnvalue['minscore'] = 0; | |
638 | if (!empty($this->moduleinstance->grade)) { | |
639 | $returnvalue['modulegrade'] = $this->moduleinstance->grade; | |
640 | } | |
641 | return $returnvalue; | |
642 | } | |
643 | } | |
644 | ||
645 | /** | |
646 | * Class to manage one guide grading instance. Stores information and performs actions like | |
647 | * update, copy, validate, submit, etc. | |
648 | * | |
649 | * @package gradingform_guide | |
650 | * @copyright 2012 Dan Marsden <dan@danmarsden.com> | |
651 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
652 | */ | |
653 | class gradingform_guide_instance extends gradingform_instance { | |
654 | ||
655 | /** @var array */ | |
656 | protected $guide; | |
657 | ||
658 | /** @var array An array of validation errors */ | |
659 | protected $validationerrors = array(); | |
660 | ||
661 | /** | |
662 | * Deletes this (INCOMPLETE) instance from database. | |
663 | */ | |
664 | public function cancel() { | |
665 | global $DB; | |
666 | parent::cancel(); | |
667 | $DB->delete_records('gradingform_guide_fillings', array('instanceid' => $this->get_id())); | |
668 | } | |
669 | ||
670 | /** | |
671 | * Duplicates the instance before editing (optionally substitutes raterid and/or itemid with | |
672 | * the specified values) | |
673 | * | |
674 | * @param int $raterid value for raterid in the duplicate | |
675 | * @param int $itemid value for itemid in the duplicate | |
676 | * @return int id of the new instance | |
677 | */ | |
678 | public function copy($raterid, $itemid) { | |
679 | global $DB; | |
680 | $instanceid = parent::copy($raterid, $itemid); | |
681 | $currentgrade = $this->get_guide_filling(); | |
682 | foreach ($currentgrade['criteria'] as $criterionid => $record) { | |
683 | $params = array('instanceid' => $instanceid, 'criterionid' => $criterionid, | |
684 | 'score' => $record['score'], 'remark' => $record['remark'], 'remarkformat' => $record['remarkformat']); | |
685 | $DB->insert_record('gradingform_guide_fillings', $params); | |
686 | } | |
687 | return $instanceid; | |
688 | } | |
689 | ||
690 | /** | |
691 | * Validates that guide is fully completed and contains valid grade on each criterion | |
692 | * | |
693 | * @param array $elementvalue value of element as came in form submit | |
694 | * @return boolean true if the form data is validated and contains no errors | |
695 | */ | |
696 | public function validate_grading_element($elementvalue) { | |
697 | $criteria = $this->get_controller()->get_definition()->guide_criteria; | |
698 | if (!isset($elementvalue['criteria']) || !is_array($elementvalue['criteria']) || | |
699 | count($elementvalue['criteria']) < count($criteria)) { | |
700 | return false; | |
701 | } | |
702 | // Reset validation errors. | |
703 | $this->validationerrors = null; | |
704 | foreach ($criteria as $id => $criterion) { | |
705 | if (!isset($elementvalue['criteria'][$id]['score']) | |
706 | || $criterion['maxscore'] < $elementvalue['criteria'][$id]['score'] | |
cfc73aa7 DM |
707 | || !is_numeric($elementvalue['criteria'][$id]['score']) |
708 | || $elementvalue['criteria'][$id]['score'] < 0) { | |
77143217 DM |
709 | $this->validationerrors[$id]['score'] = $elementvalue['criteria'][$id]['score']; |
710 | } | |
711 | } | |
712 | if (!empty($this->validationerrors)) { | |
713 | return false; | |
714 | } | |
715 | return true; | |
716 | } | |
717 | ||
718 | /** | |
719 | * Retrieves from DB and returns the data how this guide was filled | |
720 | * | |
721 | * @param bool $force whether to force DB query even if the data is cached | |
722 | * @return array | |
723 | */ | |
724 | public function get_guide_filling($force = false) { | |
725 | global $DB; | |
726 | if ($this->guide === null || $force) { | |
727 | $records = $DB->get_records('gradingform_guide_fillings', array('instanceid' => $this->get_id())); | |
728 | $this->guide = array('criteria' => array()); | |
729 | foreach ($records as $record) { | |
730 | $record->score = (float)$record->score; // Strip trailing 0. | |
731 | $this->guide['criteria'][$record->criterionid] = (array)$record; | |
732 | } | |
733 | } | |
734 | return $this->guide; | |
735 | } | |
736 | ||
737 | /** | |
738 | * Updates the instance with the data received from grading form. This function may be | |
739 | * called via AJAX when grading is not yet completed, so it does not change the | |
740 | * status of the instance. | |
741 | * | |
742 | * @param array $data | |
743 | */ | |
744 | public function update($data) { | |
745 | global $DB; | |
746 | $currentgrade = $this->get_guide_filling(); | |
747 | parent::update($data); | |
748 | ||
749 | foreach ($data['criteria'] as $criterionid => $record) { | |
750 | if (!array_key_exists($criterionid, $currentgrade['criteria'])) { | |
751 | $newrecord = array('instanceid' => $this->get_id(), 'criterionid' => $criterionid, | |
752 | 'score' => $record['score'], 'remarkformat' => FORMAT_MOODLE); | |
753 | if (isset($record['remark'])) { | |
754 | $newrecord['remark'] = $record['remark']; | |
755 | } | |
756 | $DB->insert_record('gradingform_guide_fillings', $newrecord); | |
757 | } else { | |
758 | $newrecord = array('id' => $currentgrade['criteria'][$criterionid]['id']); | |
759 | foreach (array('score', 'remark'/*, 'remarkformat' TODO */) as $key) { | |
760 | if (isset($record[$key]) && $currentgrade['criteria'][$criterionid][$key] != $record[$key]) { | |
761 | $newrecord[$key] = $record[$key]; | |
762 | } | |
763 | } | |
764 | if (count($newrecord) > 1) { | |
765 | $DB->update_record('gradingform_guide_fillings', $newrecord); | |
766 | } | |
767 | } | |
768 | } | |
769 | foreach ($currentgrade['criteria'] as $criterionid => $record) { | |
770 | if (!array_key_exists($criterionid, $data['criteria'])) { | |
771 | $DB->delete_records('gradingform_guide_fillings', array('id' => $record['id'])); | |
772 | } | |
773 | } | |
774 | $this->get_guide_filling(true); | |
775 | } | |
776 | ||
777 | /** | |
778 | * Calculates the grade to be pushed to the gradebook | |
779 | * | |
780 | * @return int the valid grade from $this->get_controller()->get_grade_range() | |
781 | */ | |
782 | public function get_grade() { | |
783 | global $DB, $USER; | |
784 | $grade = $this->get_guide_filling(); | |
785 | ||
786 | if (!($scores = $this->get_controller()->get_min_max_score()) || $scores['maxscore'] <= $scores['minscore']) { | |
787 | return -1; | |
788 | } | |
789 | ||
790 | $graderange = array_keys($this->get_controller()->get_grade_range()); | |
791 | if (empty($graderange)) { | |
792 | return -1; | |
793 | } | |
794 | sort($graderange); | |
795 | $mingrade = $graderange[0]; | |
796 | $maxgrade = $graderange[count($graderange) - 1]; | |
797 | ||
798 | $curscore = 0; | |
799 | foreach ($grade['criteria'] as $record) { | |
800 | $curscore += $record['score']; | |
801 | } | |
802 | return round(($curscore-$scores['minscore'])/($scores['maxscore']-$scores['minscore'])* | |
803 | ($maxgrade-$mingrade), 0) + $mingrade; | |
804 | } | |
805 | ||
806 | /** | |
807 | * Returns html for form element of type 'grading'. | |
808 | * | |
809 | * @param moodle_page $page | |
810 | * @param MoodleQuickForm_grading $gradingformelement | |
811 | * @return string | |
812 | */ | |
813 | public function render_grading_element($page, $gradingformelement) { | |
814 | if (!$gradingformelement->_flagFrozen) { | |
815 | $module = array('name'=>'gradingform_guide', 'fullpath'=>'/grade/grading/form/guide/js/guide.js'); | |
816 | $page->requires->js_init_call('M.gradingform_guide.init', array( | |
817 | array('name' => $gradingformelement->getName())), true, $module); | |
818 | $mode = gradingform_guide_controller::DISPLAY_EVAL; | |
819 | } else { | |
820 | if ($gradingformelement->_persistantFreeze) { | |
821 | $mode = gradingform_guide_controller::DISPLAY_EVAL_FROZEN; | |
822 | } else { | |
823 | $mode = gradingform_guide_controller::DISPLAY_REVIEW; | |
824 | } | |
825 | } | |
826 | $criteria = $this->get_controller()->get_definition()->guide_criteria; | |
827 | $comments = $this->get_controller()->get_definition()->guide_comment; | |
828 | $options = $this->get_controller()->get_options(); | |
829 | $value = $gradingformelement->getValue(); | |
830 | $html = ''; | |
831 | if ($value === null) { | |
832 | $value = $this->get_guide_filling(); | |
833 | } else if (!$this->validate_grading_element($value)) { | |
834 | $html .= html_writer::tag('div', get_string('guidenotcompleted', 'gradingform_guide'), | |
835 | array('class' => 'gradingform_guide-error')); | |
836 | if (!empty($this->validationerrors)) { | |
837 | foreach ($this->validationerrors as $id => $err) { | |
838 | $a = new stdClass(); | |
839 | $a->criterianame = $criteria[$id]['shortname']; | |
840 | $a->maxscore = $criteria[$id]['maxscore']; | |
841 | $html .= html_writer::tag('div', get_string('err_scoreinvalid', 'gradingform_guide', $a), | |
842 | array('class' => 'gradingform_guide-error')); | |
843 | } | |
844 | } | |
845 | } | |
846 | $currentinstance = $this->get_current_instance(); | |
847 | if ($currentinstance && $currentinstance->get_status() == gradingform_instance::INSTANCE_STATUS_NEEDUPDATE) { | |
848 | $html .= html_writer::tag('div', get_string('needregrademessage', 'gradingform_guide'), | |
849 | array('class' => 'gradingform_guide-regrade')); | |
850 | } | |
851 | $haschanges = false; | |
852 | if ($currentinstance) { | |
853 | $curfilling = $currentinstance->get_guide_filling(); | |
854 | foreach ($curfilling['criteria'] as $criterionid => $curvalues) { | |
855 | $value['criteria'][$criterionid]['score'] = $curvalues['score']; | |
856 | $newremark = null; | |
857 | $newscore = null; | |
858 | if (isset($value['criteria'][$criterionid]['remark'])) { | |
859 | $newremark = $value['criteria'][$criterionid]['remark']; | |
860 | } | |
861 | if (isset($value['criteria'][$criterionid]['score'])) { | |
862 | $newscore = $value['criteria'][$criterionid]['score']; | |
863 | } | |
864 | if ($newscore != $curvalues['score'] || $newremark != $curvalues['remark']) { | |
865 | $haschanges = true; | |
866 | } | |
867 | } | |
868 | } | |
869 | if ($this->get_data('isrestored') && $haschanges) { | |
870 | $html .= html_writer::tag('div', get_string('restoredfromdraft', 'gradingform_guide'), | |
871 | array('class' => 'gradingform_guide-restored')); | |
872 | } | |
873 | if (!empty($options['showdescriptionteacher'])) { | |
874 | $html .= html_writer::tag('div', $this->get_controller()->get_formatted_description(), | |
875 | array('class' => 'gradingform_guide-description')); | |
876 | } | |
877 | $html .= $this->get_controller()->get_renderer($page)->display_guide($criteria, $comments, $options, $mode, | |
878 | $gradingformelement->getName(), $value, $this->validationerrors); | |
879 | return $html; | |
880 | } | |
9fdbd69e | 881 | } |