Trivial rename of the active method selector param
[moodle.git] / grade / grading / form / rubric / lib.php
1 <?php
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/>.
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  */
27 defined('MOODLE_INTERNAL') || die();
29 require_once($CFG->dirroot.'/grade/grading/form/lib.php');
31 /**
32  * This controller encapsulates the rubric grading logic
33  */
34 class gradingform_rubric_controller extends gradingform_controller {
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)
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'),
55             $this->get_editor_url(), settings_navigation::TYPE_CUSTOM,
56             null, null, new pix_icon('icon', '', 'gradingform_rubric'));
57     }
59     /**
60      * Saves the rubric definition into the database
61      *
62      * @see parent::update_definition()
63      * @param stdClass $newdefinition rubric definition data as coming from {@link self::postupdate_definition_data()}
64      * @param int|null $usermodified optional userid of the author of the definition, defaults to the current user
65      */
66     public function update_definition(stdClass $newdefinition, $usermodified = null) {
67         global $DB;
69         // firstly update the common definition data in the {grading_definition} table
70         parent::update_definition($newdefinition, $usermodified);
71         // reload the definition from the database
72         $this->load_definition();
73         $currentdefinition = $this->get_definition();
75         // update current data
76         $haschanges = false;
77         if (empty($newdefinition->rubric_criteria)) {
78             $newcriteria = array();
79         } else {
80             $newcriteria = $newdefinition->rubric_criteria; // new ones to be saved
81         }
82         $currentcriteria = $currentdefinition->rubric_criteria;
83         $criteriafields = array('sortorder', 'description', 'descriptionformat');
84         $levelfields = array('score', 'definition', 'definitionformat');
85         foreach ($newcriteria as $id => $criterion) {
86             // get list of submitted levels
87             $levelsdata = array();
88             if (array_key_exists('levels', $criterion)) {
89                 $levelsdata = $criterion['levels'];
90             }
91             if (preg_match('/^NEWID\d+$/', $id)) {
92                 // insert criterion into DB
93                 $data = array('formid' => $this->definition->id, 'descriptionformat' => FORMAT_MOODLE); // TODO format is not supported yet
94                 foreach ($criteriafields as $key) {
95                     if (array_key_exists($key, $criterion)) {
96                         $data[$key] = $criterion[$key];
97                     }
98                 }
99                 $id = $DB->insert_record('gradingform_rubric_criteria', $data);
100                 $haschanges = true;
101             } else {
102                 // update criterion in DB
103                 $data = array();
104                 foreach ($criteriafields as $key) {
105                     if (array_key_exists($key, $criterion) && $criterion[$key] != $currentcriteria[$id][$key]) {
106                         $data[$key] = $criterion[$key];
107                     }
108                 }
109                 if (!empty($data)) {
110                     // update only if something is changed
111                     $data['id'] = $id;
112                     $DB->update_record('gradingform_rubric_criteria', $data);
113                     $haschanges = true;
114                 }
115                 // remove deleted levels from DB
116                 foreach (array_keys($currentcriteria[$id]['levels']) as $levelid) {
117                     if (!array_key_exists($levelid, $levelsdata)) {
118                         $DB->delete_records('gradingform_rubric_levels', array('id' => $levelid));
119                         $haschanges = true;
120                     }
121                 }
122             }
123             foreach ($levelsdata as $levelid => $level) {
124                 if (preg_match('/^NEWID\d+$/', $levelid)) {
125                     // insert level into DB
126                     $data = array('criterionid' => $id, 'definitionformat' => FORMAT_MOODLE); // TODO format is not supported yet
127                     foreach ($levelfields as $key) {
128                         if (array_key_exists($key, $level)) {
129                             $data[$key] = $level[$key];
130                         }
131                     }
132                     $levelid = $DB->insert_record('gradingform_rubric_levels', $data);
133                     $haschanges = true;
134                 } else {
135                     // update level in DB
136                     $data = array();
137                     foreach ($levelfields as $key) {
138                         if (array_key_exists($key, $level) && $level[$key] != $currentcriteria[$id]['levels'][$levelid][$key]) {
139                             $data[$key] = $level[$key];
140                         }
141                     }
142                     if (!empty($data)) {
143                         // update only if something is changed
144                         $data['id'] = $levelid;
145                         $DB->update_record('gradingform_rubric_levels', $data);
146                         $haschanges = true;
147                     }
148                 }
149             }
150         }
151         // remove deleted criteria from DB
152         foreach (array_keys($currentcriteria) as $id) {
153             if (!array_key_exists($id, $newcriteria)) {
154                 $DB->delete_records('gradingform_rubric_criteria', array('id' => $id));
155                 $DB->delete_records('gradingform_rubric_levels', array('criterionid' => $id));
156                 $haschanges = true;
157             }
158         }
159         $this->load_definition();
160     }
162     /**
163      * Loads the rubric form definition if it exists
164      *
165      * There is a new array called 'rubric_criteria' appended to the list of parent's definition properties.
166      */
167     protected function load_definition() {
168         global $DB;
170         $sql = "SELECT gd.*,
171                        rc.id AS rcid, rc.sortorder AS rcsortorder, rc.description AS rcdescription, rc.descriptionformat AS rcdescriptionformat,
172                        rl.id AS rlid, rl.score AS rlscore, rl.definition AS rldefinition, rl.definitionformat AS rldefinitionformat
173                   FROM {grading_definitions} gd
174              LEFT JOIN {gradingform_rubric_criteria} rc ON (rc.formid = gd.id)
175              LEFT JOIN {gradingform_rubric_levels} rl ON (rl.criterionid = rc.id)
176                  WHERE gd.areaid = :areaid AND gd.method = :method
177               ORDER BY rc.sortorder,rl.score";
178         $params = array('areaid' => $this->areaid, 'method' => $this->get_method_name());
180         $rs = $DB->get_recordset_sql($sql, $params);
181         $this->definition = false;
182         foreach ($rs as $record) {
183             // pick the common definition data
184             if (empty($this->definition)) {
185                 $this->definition = new stdClass();
186                 foreach (array('id', 'name', 'description', 'descriptionformat', 'status', 'copiedfromid',
187                         'timecreated', 'usercreated', 'timemodified', 'usermodified', 'options') as $fieldname) {
188                     $this->definition->$fieldname = $record->$fieldname;
189                 }
190                 $this->definition->rubric_criteria = array();
191             }
192             // pick the criterion data
193             if (!empty($record->rcid) and empty($this->definition->rubric_criteria[$record->rcid])) {
194                 foreach (array('id', 'sortorder', 'description', 'descriptionformat') as $fieldname) {
195                     $this->definition->rubric_criteria[$record->rcid][$fieldname] = $record->{'rc'.$fieldname};
196                 }
197                 $this->definition->rubric_criteria[$record->rcid]['levels'] = array();
198             }
199             // pick the level data
200             if (!empty($record->rlid)) {
201                 foreach (array('id', 'score', 'definition', 'definitionformat') as $fieldname) {
202                     $this->definition->rubric_criteria[$record->rcid]['levels'][$record->rlid][$fieldname] = $record->{'rl'.$fieldname};
203                 }
204             }
205         }
206         $rs->close();
207     }
209     /**
210      * Converts the current definition into an object suitable for the editor form's set_data()
211      *
212      * @return stdClass
213      */
214     public function get_definition_for_editing() {
216         $definition = $this->get_definition();
217         $properties = new stdClass();
218         $properties->areaid = $this->areaid;
219         if ($definition) {
220             foreach (array('id', 'name', 'description', 'descriptionformat', 'options', 'status') as $key) {
221                 $properties->$key = $definition->$key;
222             }
223             $options = self::description_form_field_options($this->get_context());
224             $properties = file_prepare_standard_editor($properties, 'description', $options, $this->get_context(),
225                 'gradingform_rubric', 'definition_description', $definition->id);
226         }
227         if (!empty($definition->rubric_criteria)) {
228             $properties->rubric_criteria = $definition->rubric_criteria;
229         } else {
230             $properties->rubric_criteria = array();
231         }
233         return $properties;
234     }
236     public function get_grading($raterid, $itemid) {
237         global $DB;
238         $sql = "SELECT f.id, f.criterionid, f.levelid, f.remark, f.remarkformat
239                     FROM {grading_instances} i, {gradingform_rubric_fillings} f
240                     WHERE i.formid = :formid ".
241                     "AND i.raterid = :raterid ".
242                     "AND i.itemid = :itemid
243                     AND i.id = f.forminstanceid";
244         $params = array('formid' => $this->definition->id, 'itemid' => $itemid, 'raterid' => $raterid);
245         $rs = $DB->get_recordset_sql($sql, $params);
246         $grading = array();
247         foreach ($rs as $record) {
248             if ($record->levelid) {
249                 $grading[$record->criterionid] = $record->levelid;
250             }
251             // TODO: remarks
252         }
253         return $grading;
254     }
256     /**
257      * Converts the rubric data to the gradebook score 0-100
258      */
259     protected function calculate_grade($grade, $itemid) {
260         if (!$this->validate_grading_element($grade, $itemid)) {
261             return -1;
262         }
264         $minscore = 0;
265         $maxscore = 0;
266         foreach ($this->definition->rubric_criteria as $id => $criterion) {
267             $keys = array_keys($criterion['levels']);
268             // TODO array_reverse($keys) if levels are sorted DESC
269             $minscore += $criterion['levels'][$keys[0]]['score'];
270             $maxscore += $criterion['levels'][$keys[sizeof($keys)-1]]['score'];
271         }
273         if ($maxscore == 0) {
274             return -1;
275         }
277         $curscore = 0;
278         foreach ($grade as $id => $levelid) {
279             $curscore += $this->definition->rubric_criteria[$id]['levels'][$levelid]['score'];
280         }
281         return $curscore/$maxscore*100; // TODO mapping
282     }
284     /**
285      * Saves non-js data and returns the gradebook grade
286      */
287     public function save_and_get_grade($raterid, $itemid, $formdata) {
288         global $DB, $USER;
289         $instance = $this->prepare_instance($raterid, $itemid);
290         $currentgrade = $this->get_grading($raterid, $itemid);
291         if (!is_array($formdata)) {
292             return $this->calculate_grade($currentgrade, $itemid);
293         }
294         foreach ($formdata as $criterionid => $levelid) {
295             $params = array('forminstanceid' => $instance->id, 'criterionid' => $criterionid);
296             if (!array_key_exists($criterionid, $currentgrade)) {
297                 $DB->insert_record('gradingform_rubric_fillings', $params + array('levelid' => $levelid));
298             } else if ($currentgrade[$criterionid] != $levelid) {
299                 $DB->set_field('gradingform_rubric_fillings', 'levelid', $levelid, $params);
300             }
301         }
302         foreach ($currentgrade as $criterionid => $levelid) {
303             if (!array_key_exists($criterionid, $formdata)) {
304                 $params = array('forminstanceid' => $instance->id, 'criterionid' => $criterionid);
305                 $DB->delete_records('gradingform_rubric_fillings', $params);
306             }
307         }
308         // TODO: remarks
309         return $this->calculate_grade($formdata, $itemid);
310     }
312     /**
313      * Returns html for form element
314      */
315     public function to_html($gradingformelement) {
316         global $PAGE, $USER;
317         if (!$gradingformelement->_flagFrozen) {
318             $module = array('name'=>'gradingform_rubric', 'fullpath'=>'/grade/grading/form/rubric/js/rubric.js');
319             $PAGE->requires->js_init_call('M.gradingform_rubric.init', array(array('name' => $gradingformelement->getName(), 'criteriontemplate' =>'', 'leveltemplate' => '')), true, $module);
320             $mode = self::DISPLAY_EVAL;
321         } else {
322             if ($this->_persistantFreeze) {
323                 $mode = gradingform_rubric_controller::DISPLAY_EVAL_FROZEN;
324             } else {
325                 $mode = gradingform_rubric_controller::DISPLAY_REVIEW;
326             }
327         }
328         $criteria = $this->definition->rubric_criteria;
329         $submissionid = $gradingformelement->get_grading_attribute('submissionid');
330         $raterid = $USER->id; // TODO - this is very strange!
331         $value = $gradingformelement->getValue();
332         if ($value === null) {
333             $value = $this->get_grading($raterid, $submissionid); // TODO maybe implement in form->set_data() ?
334         }
335         return $this->get_renderer($PAGE)->display_rubric($criteria, $mode, $gradingformelement->getName(), $value);
336     }
338     /**
339      * Returns html for form element
340      */
341     public function to_html_old($gradingformelement) {
342         global $PAGE, $USER;
343         //TODO move to renderer
345         //$gradingrenderer = $this->prepare_renderer($PAGE);
346         $html = '';
347         $elementname = $gradingformelement->getName();
348         $elementvalue = $gradingformelement->getValue();
349         $submissionid = $gradingformelement->get_grading_attribute('submissionid');
350         $raterid = $USER->id; // TODO - this is very strange!
351         $html .= "assessing submission $submissionid<br />";
352         //$html .= html_writer::empty_tag('input', array('type' => 'text', 'name' => $elementname.'[grade]', 'size' => '20', 'value' => $elementvalue['grade']));
354         if (!$gradingformelement->_flagFrozen) {
355             $module = array('name'=>'gradingform_rubric', 'fullpath'=>'/grade/grading/form/rubric/js/rubric.js');
356             $PAGE->requires->js_init_call('M.gradingform_rubric.init', array(array('name' => $gradingformelement->getName(), 'criteriontemplate' =>'', 'leveltemplate' => '')), true, $module);
357         }
358         $criteria = $this->definition->rubric_criteria;
360         $html .= html_writer::start_tag('div', array('id' => 'rubric-'.$gradingformelement->getName(), 'class' => 'form_rubric evaluate'));
361         $criteria_cnt = 0;
363         $value = $gradingformelement->getValue();
364         if ($value === null) {
365             $value = $this->get_grading($raterid, $submissionid); // TODO maybe implement in form->set_data() ?
366         }
368         foreach ($criteria as $criterionid => $criterion) {
369             $html .= html_writer::start_tag('div', array('class' => 'criterion'.$this->get_css_class_suffix($criteria_cnt++, count($criteria)-1)));
370             $html .= html_writer::tag('div', $criterion['description'], array('class' => 'description')); // TODO descriptionformat
371             $html .= html_writer::start_tag('div', array('class' => 'levels'));
372             $level_cnt = 0;
373             foreach ($criterion['levels'] as $levelid => $level) {
374                 $checked = (is_array($value) && array_key_exists($criterionid, $value) && ((int)$value[$criterionid] === $levelid));
375                 $classsuffix = $this->get_css_class_suffix($level_cnt++, count($criterion['levels'])-1);
376                 if ($checked) {
377                     $classsuffix .= ' checked';
378                 }
379                 $html .= html_writer::start_tag('div', array('id' => $gradingformelement->getName().'-'.$criterionid.'-levels-'.$levelid, 'class' => 'level'.$classsuffix));
380                 $input = html_writer::empty_tag('input', array('type' => 'radio', 'name' => $gradingformelement->getName().'['.$criterionid.']', 'value' => $levelid) +
381                     ($checked ? array('checked' => 'checked') : array())); // TODO rewrite
382                 $html .= html_writer::tag('div', $input, array('class' => 'radio'));
383                 $html .= html_writer::tag('div', $level['definition'], array('class' => 'definition')); // TODO definitionformat
384                 $html .= html_writer::tag('div', (float)$level['score'].' pts', array('class' => 'score'));  //TODO span, get_string
385                 $html .= html_writer::end_tag('div'); // .level
386             }
387             $html .= html_writer::end_tag('div'); // .levels
388             $html .= html_writer::end_tag('div'); // .criterion
389         }
390         $html .= html_writer::end_tag('div'); // .rubric
391         return $html;
393     }
395     private function get_css_class_suffix($cnt, $maxcnt) {
396         $class = '';
397         if ($cnt == 0) {
398             $class .= ' first';
399         }
400         if ($cnt == $maxcnt) {
401             $class .= ' last';
402         }
403         if ($cnt%2) {
404             $class .= ' odd';
405         } else {
406             $class .= ' even';
407         }
408         return $class;
409     }
411     // TODO the following functions may be moved to parent:
413     /**
414      * @return array options for the form description field
415      */
416     public static function description_form_field_options($context) {
417         global $CFG;
418         return array(
419             'maxfiles' => -1,
420             'maxbytes' => get_max_upload_file_size($CFG->maxbytes),
421             'context'  => $context,
422         );
423     }
425     public function get_formatted_description() {
426         if (!$this->definition) {
427             return null;
428         }
429         $context = $this->get_context();
431         $options = self::description_form_field_options($this->get_context());
432         $description = file_rewrite_pluginfile_urls($this->definition->description, 'pluginfile.php', $context->id,
433             'gradingform_rubric', 'definition_description', $this->definition->id, $options);
435         $formatoptions = array(
436             'noclean' => false,
437             'trusted' => false,
438             'filter' => true,
439             'context' => $context
440         );
441         return format_text($description, $this->definition->descriptionformat, $formatoptions);
442     }
444     /**
445      * Converts the rubric definition data from the submitted form back to the form
446      * suitable for storing in database
447      */
448     public function postupdate_definition_data($data) {
449         if (!$this->definition) {
450             return $data;
451         }
452         $options = self::description_form_field_options($this->get_context());
453         $data = file_postupdate_standard_editor($data, 'description', $options, $this->get_context(),
454             'gradingform_rubric', 'definition_description', $this->definition->id);
455             // TODO change filearea for embedded files in grading_definition.description
456         return $data;
457     }
459     public function is_form_available($foruserid = null) {
460         return true;
461         // TODO this is temporary for testing!
462     }
464     /**
465      * Returns the error message displayed in case of validation failed
466      *
467      * @see validate_grading_element
468      */
469     public function default_validation_error_message() {
470         return 'The rubric is incomplete'; //TODO string
471     }
473     /**
474      * Validates that rubric is fully completed and contains valid grade on each criterion
475      */
476     public function validate_grading_element($elementvalue, $itemid) {
477         // TODO: if there is nothing selected in rubric, we don't enter this function at all :(
478         $criteria = $this->definition->rubric_criteria;
479         if (!is_array($elementvalue) || sizeof($elementvalue) < sizeof($criteria)) {
480             return false;
481         }
482         foreach ($criteria as $id => $criterion) {
483             if (!array_key_exists($id, $elementvalue) || !array_key_exists($elementvalue[$id], $criterion['levels'])) {
484                 return false;
485             }
486         }
487         return true;
488     }
490     /**
491      * Returns the rubric plugin renderer
492      *
493      * @param moodle_page $page the target page
494      * @return renderer_base
495      */
496     public function get_renderer(moodle_page $page) {
497         return $page->get_renderer('gradingform_'. $this->get_method_name());
498     }
500     /**
501      * Returns the HTML code displaying the preview of the grading form
502      *
503      * @param moodle_page $page the target page
504      * @return string
505      */
506     public function render_preview(moodle_page $page) {
508         // use the parent's method to render the common information about the form
509         $header = parent::render_preview($page);
511         // append the rubric itself, using own renderer
512         $output = $this->get_renderer($page);
513         // todo something like $rubric = $output->render_preview($this);
514         $rubric = '[[TODO RUBRIC PREVIEW]]';
516         return $header . $rubric;
517     }