MDL-29794: fixed bug with description cloning
[moodle.git] / grade / grading / form / rubric / lib.php
CommitLineData
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
27defined('MOODLE_INTERNAL') || die();
28
fe817d87 29require_once($CFG->dirroot.'/grade/grading/form/lib.php');
9b8550f8
DM
30
31/**
32 * This controller encapsulates the rubric grading logic
33 */
21d37aa6 34class 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
41 const DISPLAY_REVIEW = 6; // Dispaly filled rubric (i.e. students see their grades)
9b8550f8
DM
42
43 /**
44 * Extends the module settings navigation with the rubric grading settings
45 *
46 * This function is called when the context for the page is an activity module with the
47 * FEATURE_ADVANCED_GRADING, the user has the permission moodle/grade:managegradingforms
48 * and there is an area with the active grading method set to 'rubric'.
49 *
50 * @param settings_navigation $settingsnav {@link settings_navigation}
51 * @param navigation_node $node {@link navigation_node}
52 */
53 public function extend_settings_navigation(settings_navigation $settingsnav, navigation_node $node=null) {
54 $node->add(get_string('definerubric', 'gradingform_rubric'),
6832a102 55 $this->get_editor_url(), settings_navigation::TYPE_CUSTOM,
9b8550f8
DM
56 null, null, new pix_icon('icon', '', 'gradingform_rubric'));
57 }
21d37aa6 58
c586d2bf 59 /**
fe817d87 60 * Saves the rubric definition into the database
c586d2bf 61 *
fe817d87 62 * @see parent::update_definition()
9e2eca0f 63 * @param stdClass $newdefinition rubric definition data as coming from gradingform_rubric_editrubric::get_data()
fe817d87 64 * @param int|null $usermodified optional userid of the author of the definition, defaults to the current user
c586d2bf 65 */
fe817d87 66 public function update_definition(stdClass $newdefinition, $usermodified = null) {
c586d2bf 67 global $DB;
c586d2bf 68
fe817d87 69 // firstly update the common definition data in the {grading_definition} table
9e2eca0f
MG
70 if ($this->definition === false) {
71 // if definition does not exist yet, create a blank one with only required fields set
72 // (we need id to save files embedded in description)
73 parent::update_definition((object)array('descriptionformat' => FORMAT_MOODLE), $usermodified);
74 parent::load_definition();
75 }
76 $options = self::description_form_field_options($this->get_context());
77 $newdefinition = file_postupdate_standard_editor($newdefinition, 'description', $options, $this->get_context(),
78 'gradingform_rubric', 'definition_description', $this->definition->id);
fe817d87 79 parent::update_definition($newdefinition, $usermodified);
9e2eca0f 80
fe817d87 81 // reload the definition from the database
9e2eca0f 82 $currentdefinition = $this->get_definition(true);
fe817d87 83
9e2eca0f 84 // update rubric data
fe817d87
DM
85 $haschanges = false;
86 if (empty($newdefinition->rubric_criteria)) {
87 $newcriteria = array();
c586d2bf 88 } else {
fe817d87 89 $newcriteria = $newdefinition->rubric_criteria; // new ones to be saved
c586d2bf 90 }
fe817d87
DM
91 $currentcriteria = $currentdefinition->rubric_criteria;
92 $criteriafields = array('sortorder', 'description', 'descriptionformat');
93 $levelfields = array('score', 'definition', 'definitionformat');
94 foreach ($newcriteria as $id => $criterion) {
95 // get list of submitted levels
96 $levelsdata = array();
97 if (array_key_exists('levels', $criterion)) {
98 $levelsdata = $criterion['levels'];
99 }
100 if (preg_match('/^NEWID\d+$/', $id)) {
101 // insert criterion into DB
102 $data = array('formid' => $this->definition->id, 'descriptionformat' => FORMAT_MOODLE); // TODO format is not supported yet
103 foreach ($criteriafields as $key) {
104 if (array_key_exists($key, $criterion)) {
105 $data[$key] = $criterion[$key];
106 }
107 }
108 $id = $DB->insert_record('gradingform_rubric_criteria', $data);
109 $haschanges = true;
110 } else {
111 // update criterion in DB
112 $data = array();
113 foreach ($criteriafields as $key) {
114 if (array_key_exists($key, $criterion) && $criterion[$key] != $currentcriteria[$id][$key]) {
115 $data[$key] = $criterion[$key];
116 }
117 }
118 if (!empty($data)) {
119 // update only if something is changed
120 $data['id'] = $id;
121 $DB->update_record('gradingform_rubric_criteria', $data);
122 $haschanges = true;
c586d2bf 123 }
fe817d87
DM
124 // remove deleted levels from DB
125 foreach (array_keys($currentcriteria[$id]['levels']) as $levelid) {
126 if (!array_key_exists($levelid, $levelsdata)) {
127 $DB->delete_records('gradingform_rubric_levels', array('id' => $levelid));
128 $haschanges = true;
129 }
130 }
131 }
132 foreach ($levelsdata as $levelid => $level) {
133 if (preg_match('/^NEWID\d+$/', $levelid)) {
134 // insert level into DB
135 $data = array('criterionid' => $id, 'definitionformat' => FORMAT_MOODLE); // TODO format is not supported yet
136 foreach ($levelfields as $key) {
137 if (array_key_exists($key, $level)) {
138 $data[$key] = $level[$key];
c586d2bf
MG
139 }
140 }
fe817d87 141 $levelid = $DB->insert_record('gradingform_rubric_levels', $data);
c586d2bf
MG
142 $haschanges = true;
143 } else {
fe817d87 144 // update level in DB
c586d2bf 145 $data = array();
fe817d87
DM
146 foreach ($levelfields as $key) {
147 if (array_key_exists($key, $level) && $level[$key] != $currentcriteria[$id]['levels'][$levelid][$key]) {
148 $data[$key] = $level[$key];
c586d2bf
MG
149 }
150 }
151 if (!empty($data)) {
152 // update only if something is changed
fe817d87
DM
153 $data['id'] = $levelid;
154 $DB->update_record('gradingform_rubric_levels', $data);
c586d2bf 155 $haschanges = true;
c586d2bf
MG
156 }
157 }
158 }
c586d2bf 159 }
fe817d87
DM
160 // remove deleted criteria from DB
161 foreach (array_keys($currentcriteria) as $id) {
162 if (!array_key_exists($id, $newcriteria)) {
163 $DB->delete_records('gradingform_rubric_criteria', array('id' => $id));
164 $DB->delete_records('gradingform_rubric_levels', array('criterionid' => $id));
165 $haschanges = true;
166 }
c586d2bf 167 }
fe817d87 168 $this->load_definition();
c586d2bf
MG
169 }
170
171 /**
fe817d87 172 * Loads the rubric form definition if it exists
c586d2bf 173 *
fe817d87 174 * There is a new array called 'rubric_criteria' appended to the list of parent's definition properties.
c586d2bf 175 */
fe817d87 176 protected function load_definition() {
c586d2bf 177 global $DB;
fe817d87
DM
178
179 $sql = "SELECT gd.*,
180 rc.id AS rcid, rc.sortorder AS rcsortorder, rc.description AS rcdescription, rc.descriptionformat AS rcdescriptionformat,
181 rl.id AS rlid, rl.score AS rlscore, rl.definition AS rldefinition, rl.definitionformat AS rldefinitionformat
182 FROM {grading_definitions} gd
183 LEFT JOIN {gradingform_rubric_criteria} rc ON (rc.formid = gd.id)
184 LEFT JOIN {gradingform_rubric_levels} rl ON (rl.criterionid = rc.id)
185 WHERE gd.areaid = :areaid AND gd.method = :method
186 ORDER BY rc.sortorder,rl.score";
187 $params = array('areaid' => $this->areaid, 'method' => $this->get_method_name());
188
189 $rs = $DB->get_recordset_sql($sql, $params);
190 $this->definition = false;
191 foreach ($rs as $record) {
192 // pick the common definition data
5060997b 193 if ($this->definition === false) {
fe817d87
DM
194 $this->definition = new stdClass();
195 foreach (array('id', 'name', 'description', 'descriptionformat', 'status', 'copiedfromid',
196 'timecreated', 'usercreated', 'timemodified', 'usermodified', 'options') as $fieldname) {
197 $this->definition->$fieldname = $record->$fieldname;
198 }
199 $this->definition->rubric_criteria = array();
200 }
201 // pick the criterion data
202 if (!empty($record->rcid) and empty($this->definition->rubric_criteria[$record->rcid])) {
203 foreach (array('id', 'sortorder', 'description', 'descriptionformat') as $fieldname) {
204 $this->definition->rubric_criteria[$record->rcid][$fieldname] = $record->{'rc'.$fieldname};
205 }
206 $this->definition->rubric_criteria[$record->rcid]['levels'] = array();
c586d2bf 207 }
fe817d87
DM
208 // pick the level data
209 if (!empty($record->rlid)) {
210 foreach (array('id', 'score', 'definition', 'definitionformat') as $fieldname) {
211 $this->definition->rubric_criteria[$record->rcid]['levels'][$record->rlid][$fieldname] = $record->{'rl'.$fieldname};
c586d2bf
MG
212 }
213 }
214 }
fe817d87 215 $rs->close();
c586d2bf
MG
216 }
217
218 /**
fe817d87 219 * Converts the current definition into an object suitable for the editor form's set_data()
c586d2bf 220 *
fe817d87 221 * @return stdClass
c586d2bf 222 */
fe817d87
DM
223 public function get_definition_for_editing() {
224
225 $definition = $this->get_definition();
c586d2bf 226 $properties = new stdClass();
fe817d87
DM
227 $properties->areaid = $this->areaid;
228 if ($definition) {
c586d2bf 229 foreach (array('id', 'name', 'description', 'descriptionformat', 'options', 'status') as $key) {
fe817d87 230 $properties->$key = $definition->$key;
c586d2bf 231 }
c586d2bf 232 $options = self::description_form_field_options($this->get_context());
fe817d87
DM
233 $properties = file_prepare_standard_editor($properties, 'description', $options, $this->get_context(),
234 'gradingform_rubric', 'definition_description', $definition->id);
235 }
236 if (!empty($definition->rubric_criteria)) {
237 $properties->rubric_criteria = $definition->rubric_criteria;
238 } else {
239 $properties->rubric_criteria = array();
c586d2bf 240 }
c586d2bf 241
c586d2bf
MG
242 return $properties;
243 }
244
fde33804
DM
245 /**
246 * Returns the form definition suitable for cloning into another area
247 *
248 * @see parent::get_definition_copy()
249 * @param gradingform_controller $target the controller of the new copy
250 * @return stdClass definition structure to pass to the target's {@link update_definition()}
251 */
252 public function get_definition_copy(gradingform_controller $target) {
253
254 $new = parent::get_definition_copy($target);
8722a322
MG
255 $old = $this->get_definition_for_editing();
256 $new->description_editor = $old->description_editor;
fde33804
DM
257 $new->rubric_criteria = array();
258 $newcritid = 1;
259 $newlevid = 1;
260 foreach ($old->rubric_criteria as $oldcritid => $oldcrit) {
261 unset($oldcrit['id']);
262 if (isset($oldcrit['levels'])) {
263 foreach ($oldcrit['levels'] as $oldlevid => $oldlev) {
264 unset($oldlev['id']);
265 $oldcrit['levels']['NEWID'.$newlevid] = $oldlev;
266 unset($oldcrit['levels'][$oldlevid]);
267 $newlevid++;
268 }
269 } else {
270 $oldcrit['levels'] = array();
271 }
272 $new->rubric_criteria['NEWID'.$newcritid] = $oldcrit;
273 $newcritid++;
274 }
275
276 return $new;
277 }
278
c586d2bf
MG
279 // TODO the following functions may be moved to parent:
280
fe817d87
DM
281 /**
282 * @return array options for the form description field
283 */
c586d2bf
MG
284 public static function description_form_field_options($context) {
285 global $CFG;
286 return array(
287 'maxfiles' => -1,
288 'maxbytes' => get_max_upload_file_size($CFG->maxbytes),
fe817d87 289 'context' => $context,
c586d2bf
MG
290 );
291 }
292
293 public function get_formatted_description() {
5060997b 294 if ($this->definition === false) {
c586d2bf
MG
295 return null;
296 }
297 $context = $this->get_context();
298
299 $options = self::description_form_field_options($this->get_context());
fe817d87
DM
300 $description = file_rewrite_pluginfile_urls($this->definition->description, 'pluginfile.php', $context->id,
301 'gradingform_rubric', 'definition_description', $this->definition->id, $options);
c586d2bf
MG
302
303 $formatoptions = array(
304 'noclean' => false,
305 'trusted' => false,
306 'filter' => true,
307 'context' => $context
308 );
309 return format_text($description, $this->definition->descriptionformat, $formatoptions);
310 }
311
18e6298c
MG
312 public function is_form_available($foruserid = null) {
313 return true;
314 // TODO this is temporary for testing!
315 }
6798c63e 316
6832a102
DM
317 /**
318 * Returns the rubric plugin renderer
319 *
320 * @param moodle_page $page the target page
321 * @return renderer_base
322 */
323 public function get_renderer(moodle_page $page) {
324 return $page->get_renderer('gradingform_'. $this->get_method_name());
325 }
326
327 /**
328 * Returns the HTML code displaying the preview of the grading form
329 *
330 * @param moodle_page $page the target page
331 * @return string
332 */
333 public function render_preview(moodle_page $page) {
334
335 // use the parent's method to render the common information about the form
336 $header = parent::render_preview($page);
337
338 // append the rubric itself, using own renderer
339 $output = $this->get_renderer($page);
36937f02
MG
340 $criteria = $this->definition->rubric_criteria;
341 $rubric = $output->display_rubric($criteria, self::DISPLAY_PREVIEW, 'rubric');
6832a102
DM
342
343 return $header . $rubric;
344 }
671ec8f5
DM
345
346 /**
347 * Deletes the rubric definition and all the associated information
348 */
349 protected function delete_plugin_definition() {
350 global $DB;
351
352 // get the list of instances
353 $instances = array_keys($DB->get_records('grading_instances', array('formid' => $this->definition->id), '', 'id'));
354 // delete all fillings
355 $DB->delete_records_list('gradingform_rubric_fillings', 'forminstanceid', $instances);
356 // delete instances
357 $DB->delete_records_list('grading_instances', 'id', $instances);
358 // get the list of criteria records
359 $criteria = array_keys($DB->get_records('gradingform_rubric_criteria', array('formid' => $this->definition->id), '', 'id'));
360 // delete levels
361 $DB->delete_records_list('gradingform_rubric_levels', 'criterionid', $criteria);
362 // delete critera
363 $DB->delete_records_list('gradingform_rubric_criteria', 'id', $criteria);
364 }
36937f02
MG
365
366 /**
367 * Returns html code to be included in student's feedback.
368 *
369 * @param moodle_page $page
370 * @param int $itemid
9e2eca0f 371 * @param array $grading_info result of function grade_get_grades
36937f02
MG
372 * @param string $defaultcontent default string to be returned if no active grading is found
373 * @return string
374 */
9e2eca0f 375 public function render_grade($page, $itemid, $grading_info, $defaultcontent) {
36937f02
MG
376 $instances = $this->get_current_instances($itemid);
377 return $this->get_renderer($page)->display_instances($this->get_current_instances($itemid), $defaultcontent);
378 }
9b8550f8 379}
36937f02
MG
380
381/**
382 * Class to manage one rubric grading instance. Stores information and performs actions like
383 * update, copy, validate, submit, etc.
384 *
385 * @copyright 2011 Marina Glancy
386 */
387class gradingform_rubric_instance extends gradingform_instance {
388
5060997b
MG
389 protected $rubric;
390
36937f02
MG
391 /**
392 * Deletes this (INCOMPLETE) instance from database.
393 */
394 public function cancel() {
395 global $DB;
396 parent::cancel();
397 $DB->delete_records('gradingform_rubric_fillings', array('forminstanceid' => $this->get_id()));
398 }
399
400 /**
401 * Duplicates the instance before editing (optionally substitutes raterid and/or itemid with
402 * the specified values)
403 *
404 * @param int $raterid value for raterid in the duplicate
405 * @param int $itemid value for itemid in the duplicate
406 * @return int id of the new instance
407 */
408 public function copy($raterid, $itemid) {
409 global $DB;
410 $instanceid = parent::copy($raterid, $itemid);
411 $currentgrade = $this->get_rubric_filling();
5060997b
MG
412 foreach ($currentgrade['criteria'] as $criterionid => $record) {
413 $params = array('forminstanceid' => $instanceid, 'criterionid' => $criterionid,
414 'levelid' => $record['levelid'], 'remark' => $record['remark'], 'remarkformat' => $record['remarkformat']);
36937f02
MG
415 $DB->insert_record('gradingform_rubric_fillings', $params);
416 }
36937f02
MG
417 return $instanceid;
418 }
419
420 /**
421 * Validates that rubric is fully completed and contains valid grade on each criterion
422 * @return boolean true if the form data is validated and contains no errors
423 */
424 public function validate_grading_element($elementvalue) {
425 // TODO: if there is nothing selected in rubric, we don't enter this function at all :(
426 $criteria = $this->get_controller()->get_definition()->rubric_criteria;
5060997b 427 if (!isset($elementvalue['criteria']) || !is_array($elementvalue['criteria']) || sizeof($elementvalue['criteria']) < sizeof($criteria)) {
36937f02
MG
428 return false;
429 }
430 foreach ($criteria as $id => $criterion) {
5060997b
MG
431 if (!isset($elementvalue['criteria'][$id]['levelid'])
432 || !array_key_exists($elementvalue['criteria'][$id]['levelid'], $criterion['levels'])) {
36937f02
MG
433 return false;
434 }
435 }
436 return true;
437 }
438
439 /**
440 * Retrieves from DB and returns the data how this rubric was filled
441 *
5060997b 442 * @param boolean $force whether to force DB query even if the data is cached
36937f02
MG
443 * @return array
444 */
5060997b 445 public function get_rubric_filling($force = false) {
36937f02 446 global $DB;
5060997b
MG
447 if ($this->rubric === null || $force) {
448 $records = $DB->get_records('gradingform_rubric_fillings', array('forminstanceid' => $this->get_id()));
449 $this->rubric = array('criteria' => array());
450 foreach ($records as $record) {
451 $this->rubric['criteria'][$record->criterionid] = (array)$record;
36937f02 452 }
36937f02 453 }
5060997b 454 return $this->rubric;
36937f02
MG
455 }
456
457 /**
458 * Updates the instance with the data received from grading form. This function may be
459 * called via AJAX when grading is not yet completed, so it does not change the
460 * status of the instance.
fc05f222
MG
461 *
462 * @param array $data
36937f02
MG
463 */
464 public function update($data) {
465 global $DB;
466 $currentgrade = $this->get_rubric_filling();
fc05f222 467 parent::update($data);
5060997b
MG
468 foreach ($data['criteria'] as $criterionid => $record) {
469 if (!array_key_exists($criterionid, $currentgrade['criteria'])) {
470 $newrecord = array('forminstanceid' => $this->get_id(), 'criterionid' => $criterionid,
471 'levelid' => $record['levelid'], 'remark' => $record['remark'], 'remarkformat' => FORMAT_MOODLE);
472 $DB->insert_record('gradingform_rubric_fillings', $newrecord);
473 } else {
474 $newrecord = array('id' => $currentgrade['criteria'][$criterionid]['id']);
475 foreach (array('levelid', 'remark'/*, 'remarkformat' TODO */) as $key) {
476 if ($currentgrade['criteria'][$criterionid][$key] != $record[$key]) {
477 $newrecord[$key] = $record[$key];
478 }
479 }
480 if (count($newrecord) > 1) {
481 $DB->update_record('gradingform_rubric_fillings', $newrecord);
482 }
36937f02
MG
483 }
484 }
5060997b
MG
485 foreach ($currentgrade['criteria'] as $criterionid => $record) {
486 if (!array_key_exists($criterionid, $data['criteria'])) {
487 $DB->delete_records('gradingform_rubric_fillings', array('id' => $record['id']));
36937f02
MG
488 }
489 }
5060997b 490 $this->get_rubric_filling(true);
36937f02
MG
491 }
492
493 /**
494 * Calculates the grade to be pushed to the gradebook
9e2eca0f
MG
495 *
496 * @return int the valid grade from $this->get_controller()->get_grade_range()
36937f02
MG
497 */
498 public function get_grade() {
499 global $DB, $USER;
500 $grade = $this->get_rubric_filling();
501
502 $minscore = 0;
503 $maxscore = 0;
504 foreach ($this->get_controller()->get_definition()->rubric_criteria as $id => $criterion) {
505 $keys = array_keys($criterion['levels']);
9e2eca0f 506 sort($keys);
36937f02
MG
507 $minscore += $criterion['levels'][$keys[0]]['score'];
508 $maxscore += $criterion['levels'][$keys[sizeof($keys)-1]]['score'];
509 }
510
9e2eca0f
MG
511 if ($maxscore <= $minscore) {
512 return -1;
513 }
514
515 $graderange = array_keys($this->get_controller()->get_grade_range());
516 if (empty($graderange)) {
36937f02
MG
517 return -1;
518 }
9e2eca0f
MG
519 sort($graderange);
520 $mingrade = $graderange[0];
521 $maxgrade = $graderange[sizeof($graderange) - 1];
36937f02
MG
522
523 $curscore = 0;
5060997b
MG
524 foreach ($grade['criteria'] as $id => $record) {
525 $curscore += $this->get_controller()->get_definition()->rubric_criteria[$id]['levels'][$record['levelid']]['score'];
36937f02 526 }
9e2eca0f 527 return round(($curscore-$minscore)/($maxscore-$minscore)*($maxgrade-$mingrade), 0) + $mingrade; // TODO mapping
36937f02
MG
528 }
529
530 /**
531 * Returns the error message displayed in case of validation failed
532 *
533 * @return string
534 */
535 public function default_validation_error_message() {
536 return 'The rubric is incomplete'; //TODO string
537 }
538
539 /**
540 * Returns html for form element of type 'grading'.
541 *
542 * @param moodle_page $page
543 * @param MoodleQuickForm_grading $formelement
544 * @return string
545 */
546 public function render_grading_element($page, $gradingformelement) {
547 global $USER;
548 if (!$gradingformelement->_flagFrozen) {
549 $module = array('name'=>'gradingform_rubric', 'fullpath'=>'/grade/grading/form/rubric/js/rubric.js');
550 $page->requires->js_init_call('M.gradingform_rubric.init', array(array('name' => $gradingformelement->getName())), true, $module);
551 $mode = gradingform_rubric_controller::DISPLAY_EVAL;
552 } else {
553 if ($gradingformelement->_persistantFreeze) {
554 $mode = gradingform_rubric_controller::DISPLAY_EVAL_FROZEN;
555 } else {
556 $mode = gradingform_rubric_controller::DISPLAY_REVIEW;
557 }
558 }
559 $criteria = $this->get_controller()->get_definition()->rubric_criteria;
560 $value = $gradingformelement->getValue();
561 if ($value === null) {
562 $value = $this->get_rubric_filling();
563 }
564 return $this->get_controller()->get_renderer($page)->display_rubric($criteria, $mode, $gradingformelement->getName(), $value);
565 }
566}