Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Merge remote branch 'github/rubric' into rubric

  • Loading branch information...
commit 8b9ada5e195e94dc53ab6fefa2111d56b0cce2e7 2 parents 9c9cee4 + d1bc817
David Mudrák authored October 24, 2011
381  grade/grading/form/lib.php
@@ -48,7 +48,10 @@
48 48
     protected $areaid;
49 49
 
50 50
     /** @var stdClass|false the definition structure */
51  
-    protected $definition;
  51
+    protected $definition = false;
  52
+
  53
+    /** @var array graderange array of valid grades for this area. Use set_grade_range and get_grade_range to access this */
  54
+    private $graderange = null;
52 55
 
53 56
     /**
54 57
      * Do not instantinate this directly, use {@link grading_manager::get_controller()}
@@ -107,7 +110,7 @@ public function get_areaid() {
107 110
      * @return boolean
108 111
      */
109 112
     public function is_form_defined() {
110  
-        return !empty($this->definition);
  113
+        return ($this->definition !== false);
111 114
     }
112 115
 
113 116
     /**
@@ -174,10 +177,11 @@ public function extend_settings_navigation(settings_navigation $settingsnav, nav
174 177
     /**
175 178
      * Returns the grading form definition structure
176 179
      *
  180
+     * @param boolean $force whether to force loading from DB even if it was already loaded
177 181
      * @return stdClass|false definition data or false if the form is not defined yet
178 182
      */
179  
-    public function get_definition() {
180  
-        if (is_null($this->definition)) {
  183
+    public function get_definition($force = false) {
  184
+        if ($this->definition === false || $force) {
181 185
             $this->load_definition();
182 186
         }
183 187
         return $this->definition;
@@ -287,65 +291,92 @@ public function update_definition(stdClass $definition, $usermodified = null) {
287 291
     }
288 292
 
289 293
     /**
290  
-     * Makes sure there is a form instance for the given rater grading the given item
291  
-     *
292  
-     * Plugins will probably override/extend this and load additional data of how their
293  
-     * forms are filled in one complex query.
  294
+     * Returns the ACTIVE instance for this definition for the specified $raterid and $itemid
  295
+     * (if multiple raters are allowed, or only for $itemid otherwise).
294 296
      *
295  
-     * @todo this might actually become abstract method
296 297
      * @param int $raterid
297 298
      * @param int $itemid
298  
-     * @return stdClass newly created or existing record from {grading_instances}
  299
+     * @param boolean $idonly
  300
+     * @return mixed if $idonly=true returns id of the found instance, otherwise returns the instance object
299 301
      */
300  
-    public function prepare_instance($raterid, $itemid) {
  302
+    public function get_current_instance($raterid, $itemid, $idonly = false) {
301 303
         global $DB;
302  
-
303  
-        if (empty($this->definition)) {
304  
-            throw new coding_exception('Attempting to prepare an instance of non-existing grading form');
  304
+        $select = array(
  305
+                'formid'  => $this->definition->id,
  306
+                'itemid' => $itemid,
  307
+                'status'  => gradingform_instance::INSTANCE_STATUS_ACTIVE);
  308
+        if (false /* TODO $manager->allow_multiple_raters() */) {
  309
+            $select['raterid'] = $raterid;
305 310
         }
306  
-
307  
-        $current = $DB->get_record('grading_instances', array(
308  
-            'formid'  => $this->definition->id,
309  
-            'raterid' => $raterid,
310  
-            'itemid'  => $itemid), '*', IGNORE_MISSING);
311  
-
312  
-        if (empty($current)) {
313  
-            $instance = new stdClass();
314  
-            $instance->formid = $this->definition->id;
315  
-            $instance->raterid = $raterid;
316  
-            $instance->itemid = $itemid;
317  
-            $instance->timemodified = time();
318  
-            $instance->feedbackformat = FORMAT_MOODLE;
319  
-            $instance->id = $DB->insert_record('grading_instances', $instance);
320  
-            return $instance;
321  
-
  311
+        if ($idonly) {
  312
+            if ($current = $DB->get_record('grading_instances', $select, 'id', IGNORE_MISSING)) {
  313
+                return $current->id;
  314
+            }
322 315
         } else {
323  
-            return $current;
  316
+            if ($current = $DB->get_record('grading_instances', $select, '*', IGNORE_MISSING)) {
  317
+                return $this->get_instance($current);
  318
+            }
324 319
         }
  320
+        return null;
325 321
     }
326 322
 
327 323
     /**
328  
-     * Saves non-js data and returns the gradebook grade
329  
-     */
330  
-    abstract public function save_and_get_grade($raterid, $itemid, $formdata);
331  
-
332  
-    /**
333  
-     * Returns html for form element
  324
+     * Returns list of active instances for the specified $itemid
  325
+     *
  326
+     * @param int $itemid
  327
+     * @return array of gradingform_instance objects
334 328
      */
335  
-    abstract public function to_html($gradingformelement);
  329
+    public function get_current_instances($itemid) {
  330
+        global $DB;
  331
+        $conditions = array('formid'  => $this->definition->id,
  332
+                    'itemid' => $itemid,
  333
+                    'status'  => gradingform_instance::INSTANCE_STATUS_ACTIVE);
  334
+        $records = $DB->get_recordset('grading_instances', $conditions);
  335
+        $rv = array();
  336
+        foreach ($records as $record) {
  337
+            $rv[] = $this->get_instance($record);
  338
+        }
  339
+        return $rv;
  340
+    }
336 341
 
337 342
     /**
  343
+     * Returns the object of type gradingform_XXX_instance (where XXX is the plugin method name)
338 344
      *
  345
+     * @param mixed $instance id or row from grading_isntances table
  346
+     * @return gradingform_instance
339 347
      */
340  
-    public function default_validation_error_message() {
341  
-        return '';
  348
+    protected function get_instance($instance) {
  349
+        global $DB;
  350
+        if (is_scalar($instance)) {
  351
+            // instance id is passed as parameter
  352
+            $instance = $DB->get_record('grading_instances', array('id'  => $instance), '*', MUST_EXIST);
  353
+        }
  354
+        if ($instance) {
  355
+            $class = 'gradingform_'. $this->get_method_name(). '_instance';
  356
+            return new $class($this, $instance);
  357
+        }
  358
+        return null;
342 359
     }
343 360
 
344 361
     /**
  362
+     * This function is invoked when user (teacher) starts grading.
  363
+     * It creates and returns copy of the current ACTIVE instance if it exists. If this is the
  364
+     * first grading attempt, a new instance is created.
  365
+     * The status of the returned instance is INCOMPLETE
345 366
      *
  367
+     * @param int $raterid
  368
+     * @param int $itemid
  369
+     * @return gradingform_instance
346 370
      */
347  
-    public function validate_grading_element($elementvalue, $itemid) {
348  
-        return true;
  371
+    public function create_instance($raterid, $itemid = null) {
  372
+        global $DB;
  373
+        // first find if there is already an active instance for this itemid
  374
+        if ($itemid && $current = $this->get_current_instance($raterid, $itemid)) {
  375
+            return $this->get_instance($current->copy($raterid, $itemid));
  376
+        } else {
  377
+            $class = 'gradingform_'. $this->get_method_name(). '_instance';
  378
+            return $this->get_instance($class::create_new($this->definition->id, $raterid, $itemid));
  379
+        }
349 380
     }
350 381
 
351 382
     /**
@@ -426,4 +457,270 @@ protected function get_method_name() {
426 457
             throw new coding_exception('Invalid class name');
427 458
         }
428 459
     }
  460
+
  461
+    /**
  462
+     * Returns html code to be included in student's feedback.
  463
+     *
  464
+     * @param moodle_page $page
  465
+     * @param int $itemid
  466
+     * @param array $grading_info result of function grade_get_grades if plugin want to use some of their info
  467
+     * @param string $defaultcontent default string to be returned if no active grading is found or for some reason can not be shown to a user
  468
+     * @return string
  469
+     */
  470
+    public function render_grade($page, $itemid, $grading_info, $defaultcontent) {
  471
+        return $defaultcontent;
  472
+    }
  473
+
  474
+    /**
  475
+     * Sets the range of grades used in this area. This is usually either range like 0-100
  476
+     * or the scale where keys start from 1. Typical use:
  477
+     * $controller->set_grade_range(make_grades_menu($gradingtype));
  478
+     */
  479
+    public final function set_grade_range(array $graderange) {
  480
+        $this->graderange = $graderange;
  481
+    }
  482
+
  483
+    /**
  484
+     * Returns the range of grades used in this area
  485
+     * @return array
  486
+     */
  487
+    public final function get_grade_range() {
  488
+        if (empty($this->graderange)) {
  489
+            return array();
  490
+        }
  491
+        return $this->graderange;
  492
+    }
429 493
 }
  494
+
  495
+/**
  496
+ * Class to manage one grading instance. Stores information and performs actions like
  497
+ * update, copy, validate, submit, etc.
  498
+ *
  499
+ * @copyright  2011 Marina Glancy
  500
+ */
  501
+abstract class gradingform_instance {
  502
+    const INSTANCE_STATUS_ACTIVE = 1;
  503
+    const INSTANCE_STATUS_INCOMPLETE = 0;
  504
+    const INSTANCE_STATUS_ARCHIVE = 3;
  505
+
  506
+    /** @var stdClass record from table grading_instances */
  507
+    protected $data;
  508
+    /** @var gradingform_controller link to the corresponding controller */
  509
+    protected $controller;
  510
+
  511
+    /**
  512
+     * Creates an instance
  513
+     *
  514
+     * @param gradingform_controller $controller
  515
+     * @param stdClass $data
  516
+     */
  517
+    public function __construct($controller, $data) {
  518
+        $this->data = (object)$data;
  519
+        $this->controller = $controller;
  520
+    }
  521
+
  522
+    /**
  523
+     * Creates a new empty instance in DB and mark its status as INCOMPLETE
  524
+     *
  525
+     * @param int $formid
  526
+     * @param int $raterid
  527
+     * @param int $itemid
  528
+     * @return int id of the created instance
  529
+     */
  530
+    public static function create_new($formid, $raterid, $itemid) {
  531
+        global $DB;
  532
+        $instance = new stdClass();
  533
+        $instance->formid = $formid;
  534
+        $instance->raterid = $raterid;
  535
+        $instance->itemid = $itemid;
  536
+        $instance->status = self::INSTANCE_STATUS_INCOMPLETE;
  537
+        $instance->timemodified = time();
  538
+        $instance->feedbackformat = FORMAT_MOODLE;
  539
+        $instanceid = $DB->insert_record('grading_instances', $instance);
  540
+        return $instanceid;
  541
+    }
  542
+
  543
+    /**
  544
+     * Duplicates the instance before editing (optionally substitutes raterid and/or itemid with
  545
+     * the specified values)
  546
+     * Plugins may want to override this function to copy data from additional tables as well
  547
+     *
  548
+     * @param int $raterid value for raterid in the duplicate
  549
+     * @param int $itemid value for itemid in the duplicate
  550
+     * @return int id of the new instance
  551
+     */
  552
+    public function copy($raterid, $itemid) {
  553
+        global $DB;
  554
+        $data = (array)$this->data; // Cast to array to make a copy
  555
+        unset($data['id']);
  556
+        $data['raterid'] = $raterid;
  557
+        $data['itemid'] = $itemid;
  558
+        $data['timemodified'] = time();
  559
+        $data['status'] = self::INSTANCE_STATUS_INCOMPLETE;
  560
+        $instanceid = $DB->insert_record('grading_instances', $data);
  561
+        return $instanceid;
  562
+    }
  563
+
  564
+    /**
  565
+     * Returns the controller
  566
+     *
  567
+     * @return gradingform_controller
  568
+     */
  569
+    public function get_controller() {
  570
+        return $this->controller;
  571
+    }
  572
+
  573
+    /**
  574
+     * Returns instance id
  575
+     *
  576
+     * @return int
  577
+     */
  578
+    public function get_id() {
  579
+        return $this->data->id;
  580
+    }
  581
+
  582
+    /**
  583
+     * Marks the instance as ACTIVE and current active instance (if exists) as ARCHIVE
  584
+     */
  585
+    protected function make_active() {
  586
+        global $DB;
  587
+        if ($this->data->status == self::INSTANCE_STATUS_ACTIVE) {
  588
+            // already active
  589
+            return;
  590
+        }
  591
+        if (empty($this->data->itemid)) {
  592
+            throw new coding_exception('You cannot mark active the grading instance without itemid');
  593
+        }
  594
+        $currentid = $this->get_controller()->get_current_instance($this->data->raterid, $this->data->itemid, true);
  595
+        if ($currentid) {
  596
+            if ($currentid != $this->get_id()) {
  597
+                $DB->update_record('grading_instances', array('id' => $currentid, 'status' => self::INSTANCE_STATUS_ARCHIVE));
  598
+                $DB->update_record('grading_instances', array('id' => $this->get_id(), 'status' => self::INSTANCE_STATUS_ACTIVE));
  599
+            }
  600
+        } else {
  601
+            $DB->update_record('grading_instances', array('id' => $this->get_id(), 'status' => self::INSTANCE_STATUS_ACTIVE));
  602
+        }
  603
+        $this->data->status = self::INSTANCE_STATUS_ACTIVE;
  604
+    }
  605
+
  606
+    /**
  607
+     * Deletes this (INCOMPLETE) instance from database. This function is invoked on cancelling the
  608
+     * grading form and/or during cron cleanup.
  609
+     * Plugins using additional tables must override this method to remove additional data.
  610
+     * Note that if the teacher just closes the window or presses 'Back' button of the browser,
  611
+     * this function is not invoked.
  612
+     */
  613
+    public function cancel() {
  614
+        global $DB;
  615
+        // TODO what if we happen delete the ACTIVE instance, shall we rollback to the last ARCHIVE? or throw an exception?
  616
+        // TODO create cleanup cron
  617
+        $DB->delete_records('grading_instances', array('id' => $this->get_id()));
  618
+    }
  619
+
  620
+    /**
  621
+     * Updates the instance with the data received from grading form. This function may be
  622
+     * called via AJAX when grading is not yet completed, so it does not change the
  623
+     * status of the instance.
  624
+     *
  625
+     * @param array $elementvalue
  626
+     */
  627
+    public function update($elementvalue) {
  628
+        global $DB;
  629
+        $newdata = new stdClass();
  630
+        $newdata->id = $this->get_id();
  631
+        $newdata->timemodified = time();
  632
+        if (isset($elementvalue['itemid']) && $elementvalue['itemid'] != $this->data->itemid) {
  633
+            $newdata->itemid = $elementvalue['itemid'];
  634
+        }
  635
+        // TODO also update: rawgrade, feedback, feedbackformat
  636
+        $DB->update_record('grading_instances', $newdata);
  637
+        foreach ($newdata as $key => $value) {
  638
+            $this->data->$key = $value;
  639
+        }
  640
+    }
  641
+
  642
+    /**
  643
+     * Calculates the grade to be pushed to the gradebook
  644
+     *
  645
+     * @return int the valid grade from $this->get_controller()->get_grade_range()
  646
+     */
  647
+    abstract public function get_grade();
  648
+
  649
+    /**
  650
+     * Called when teacher submits the grading form:
  651
+     * updates the instance in DB, marks it as ACTIVE and returns the grade to be pushed to the gradebook.
  652
+     * $itemid must be specified here (it was not required when the instance was
  653
+     * created, because it might not existed in draft)
  654
+     *
  655
+     * @param array $elementvalue
  656
+     * @param int $itemid
  657
+     * @return int the grade on 0-100 scale
  658
+     */
  659
+    public function submit_and_get_grade($elementvalue, $itemid) {
  660
+        $elementvalue['itemid'] = $itemid;
  661
+        $this->update($elementvalue);
  662
+        $this->make_active();
  663
+        return $this->get_grade();
  664
+    }
  665
+
  666
+
  667
+    /**
  668
+     * Returns html for form element of type 'grading'. If there is a form input element
  669
+     * it must have the name $gradingformelement->getName().
  670
+     * If there are more than one input elements they MUST be elements of array with
  671
+     * name $gradingformelement->getName().
  672
+     * Example: {NAME}[myelement1], {NAME}[myelement2][sub1], {NAME}[myelement2][sub2], etc.
  673
+     * ( {NAME} is a shortcut for $gradingformelement->getName() )
  674
+     * After submitting the form the value of $_POST[{NAME}] is passed to the functions
  675
+     * validate_grading_element() and submit_and_get_grade()
  676
+     *
  677
+     * Plugins may use $gradingformelement->getValue() to get the value passed on previous
  678
+     * form submit
  679
+     *
  680
+     * When forming html it is a plugin's responsibility to analyze flags
  681
+     * $gradingformelement->_flagFrozen and $gradingformelement->_persistantFreeze:
  682
+     *
  683
+     * (_flagFrozen == false) => form element is editable
  684
+     *
  685
+     * (_flagFrozen == false && _persistantFreeze == true) => form element is not editable
  686
+     * but all values are passed as hidden elements
  687
+     *
  688
+     * (_flagFrozen == false && _persistantFreeze == false) => form element is not editable
  689
+     * and no values are passed as hidden elements
  690
+     *
  691
+     * Plugins are welcome to use AJAX in the form element. But it is strongly recommended
  692
+     * that the grading only becomes active when teacher presses 'Submit' button (the
  693
+     * method submit_and_get_grade() is invoked)
  694
+     *
  695
+     * Also client-side JS validation may be implemented here
  696
+     *
  697
+     * @see MoodleQuickForm_grading in lib/form/grading.php
  698
+     *
  699
+     * @param moodle_page $page
  700
+     * @param MoodleQuickForm_grading $gradingformelement
  701
+     * @return string
  702
+     */
  703
+    abstract function render_grading_element($page, $gradingformelement);
  704
+
  705
+    /**
  706
+     * Server-side validation of the data received from grading form.
  707
+     *
  708
+     * @param mixed $elementvalue is the scalar or array received in $_POST
  709
+     * @return boolean true if the form data is validated and contains no errors
  710
+     */
  711
+    public function validate_grading_element($elementvalue) {
  712
+        return true;
  713
+    }
  714
+
  715
+    /**
  716
+     * Returns the error message displayed if validation failed.
  717
+     * If plugin wants to display custom message, the empty string should be returned here
  718
+     * and the custom message should be output in render_grading_element()
  719
+     *
  720
+     * @see validate_grading_element()
  721
+     * @return string
  722
+     */
  723
+    public function default_validation_error_message() {
  724
+        return '';
  725
+    }
  726
+}
16  grade/grading/form/rubric/edit.php
@@ -46,16 +46,18 @@
46 46
 $PAGE->requires->js('/grade/grading/form/rubric/js/rubriceditor.js');
47 47
 
48 48
 //TODO freeze rubric editor if needed
49  
-$mform = new gradingform_rubric_editrubric(null, array('areaid' => $areaid, 'freezerubric' => optional_param('freeze', 0, PARAM_INT)));
50  
-$mform->set_data($controller->get_definition_for_editing());
  49
+$mform = new gradingform_rubric_editrubric(null, array('areaid' => $areaid, 'context' => $context, 'freezerubric' => optional_param('freeze', 0, PARAM_INT)));
  50
+$data = $controller->get_definition_for_editing();
  51
+$returnurl = optional_param('returnurl', $manager->get_management_url(), PARAM_LOCALURL);
  52
+$data->returnurl = $returnurl;
  53
+$mform->set_data($data);
51 54
 if ($mform->is_cancelled()) {
52 55
     // todo process editing cancel in a better way
53  
-    redirect($manager->get_management_url());
54  
-
55  
-} else if ($data = $mform->get_data()) {
56  
-    $data = $controller->postupdate_definition_data($data);
  56
+    redirect($returnurl);
  57
+} else if ($mform->is_submitted() && $mform->is_validated()) {
  58
+    $data = $mform->get_data();
57 59
     $controller->update_definition($data);
58  
-    redirect($PAGE->url);
  60
+    redirect($returnurl);
59 61
 }
60 62
 
61 63
 echo $OUTPUT->header();
4  grade/grading/form/rubric/edit_form.php
@@ -44,13 +44,15 @@ public function definition() {
44 44
         $form->addElement('hidden', 'areaid');
45 45
         $form->setType('areaid', PARAM_INT);
46 46
 
  47
+        $form->addElement('hidden', 'returnurl');
  48
+
47 49
         // name
48 50
         $form->addElement('text', 'name', get_string('name', 'gradingform_rubric'), array('size'=>52));
49 51
         $form->addRule('name', get_string('required'), 'required');
50 52
         $form->setType('name', PARAM_TEXT);
51 53
 
52 54
         // description
53  
-        $options = array();
  55
+        $options = gradingform_rubric_controller::description_form_field_options($this->_customdata['context']);
54 56
         $form->addElement('editor', 'description_editor', get_string('description', 'gradingform_rubric'), null, $options);
55 57
         $form->setType('description_editor', PARAM_RAW);
56 58
 
1  grade/grading/form/rubric/js/rubriceditor.js
@@ -83,6 +83,7 @@ M.gradingform_rubriceditor.editmode = function(el, editmode) {
83 83
         }
84 84
         ta.get('parentNode').one('.plainvalue').setStyle('display', 'none')
85 85
         ta.setStyle('display', 'block').setStyle('width', width).setStyle('height', height)
  86
+        ta.focus()
86 87
     }
87 88
 }
88 89
 
443  grade/grading/form/rubric/lib.php
@@ -60,19 +60,28 @@ public function extend_settings_navigation(settings_navigation $settingsnav, nav
60 60
      * Saves the rubric definition into the database
61 61
      *
62 62
      * @see parent::update_definition()
63  
-     * @param stdClass $newdefinition rubric definition data as coming from {@link self::postupdate_definition_data()}
  63
+     * @param stdClass $newdefinition rubric definition data as coming from gradingform_rubric_editrubric::get_data()
64 64
      * @param int|null $usermodified optional userid of the author of the definition, defaults to the current user
65 65
      */
66 66
     public function update_definition(stdClass $newdefinition, $usermodified = null) {
67 67
         global $DB;
68 68
 
69 69
         // firstly update the common definition data in the {grading_definition} table
  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);
70 79
         parent::update_definition($newdefinition, $usermodified);
  80
+
71 81
         // reload the definition from the database
72  
-        $this->load_definition();
73  
-        $currentdefinition = $this->get_definition();
  82
+        $currentdefinition = $this->get_definition(true);
74 83
 
75  
-        // update current data
  84
+        // update rubric data
76 85
         $haschanges = false;
77 86
         if (empty($newdefinition->rubric_criteria)) {
78 87
             $newcriteria = array();
@@ -181,7 +190,7 @@ protected function load_definition() {
181 190
         $this->definition = false;
182 191
         foreach ($rs as $record) {
183 192
             // pick the common definition data
184  
-            if (empty($this->definition)) {
  193
+            if ($this->definition === false) {
185 194
                 $this->definition = new stdClass();
186 195
                 foreach (array('id', 'name', 'description', 'descriptionformat', 'status', 'copiedfromid',
187 196
                         'timecreated', 'usercreated', 'timemodified', 'usermodified', 'options') as $fieldname) {
@@ -266,182 +275,6 @@ public function get_definition_copy(gradingform_controller $target) {
266 275
         return $new;
267 276
     }
268 277
 
269  
-    public function get_grading($raterid, $itemid) {
270  
-        global $DB;
271  
-        $sql = "SELECT f.id, f.criterionid, f.levelid, f.remark, f.remarkformat
272  
-                    FROM {grading_instances} i, {gradingform_rubric_fillings} f
273  
-                    WHERE i.formid = :formid ".
274  
-                    "AND i.raterid = :raterid ".
275  
-                    "AND i.itemid = :itemid
276  
-                    AND i.id = f.forminstanceid";
277  
-        $params = array('formid' => $this->definition->id, 'itemid' => $itemid, 'raterid' => $raterid);
278  
-        $rs = $DB->get_recordset_sql($sql, $params);
279  
-        $grading = array();
280  
-        foreach ($rs as $record) {
281  
-            if ($record->levelid) {
282  
-                $grading[$record->criterionid] = $record->levelid;
283  
-            }
284  
-            // TODO: remarks
285  
-        }
286  
-        $rs->close();
287  
-        return $grading;
288  
-    }
289  
-
290  
-    /**
291  
-     * Converts the rubric data to the gradebook score 0-100
292  
-     */
293  
-    protected function calculate_grade($grade, $itemid) {
294  
-        if (!$this->validate_grading_element($grade, $itemid)) {
295  
-            return -1;
296  
-        }
297  
-
298  
-        $minscore = 0;
299  
-        $maxscore = 0;
300  
-        foreach ($this->definition->rubric_criteria as $id => $criterion) {
301  
-            $keys = array_keys($criterion['levels']);
302  
-            // TODO array_reverse($keys) if levels are sorted DESC
303  
-            $minscore += $criterion['levels'][$keys[0]]['score'];
304  
-            $maxscore += $criterion['levels'][$keys[sizeof($keys)-1]]['score'];
305  
-        }
306  
-
307  
-        if ($maxscore == 0) {
308  
-            return -1;
309  
-        }
310  
-
311  
-        $curscore = 0;
312  
-        foreach ($grade as $id => $levelid) {
313  
-            $curscore += $this->definition->rubric_criteria[$id]['levels'][$levelid]['score'];
314  
-        }
315  
-        return $curscore/$maxscore*100; // TODO mapping
316  
-    }
317  
-
318  
-    /**
319  
-     * Saves non-js data and returns the gradebook grade
320  
-     */
321  
-    public function save_and_get_grade($raterid, $itemid, $formdata) {
322  
-        global $DB, $USER;
323  
-        $instance = $this->prepare_instance($raterid, $itemid);
324  
-        $currentgrade = $this->get_grading($raterid, $itemid);
325  
-        if (!is_array($formdata)) {
326  
-            return $this->calculate_grade($currentgrade, $itemid);
327  
-        }
328  
-        foreach ($formdata as $criterionid => $levelid) {
329  
-            $params = array('forminstanceid' => $instance->id, 'criterionid' => $criterionid);
330  
-            if (!array_key_exists($criterionid, $currentgrade)) {
331  
-                $DB->insert_record('gradingform_rubric_fillings', $params + array('levelid' => $levelid));
332  
-            } else if ($currentgrade[$criterionid] != $levelid) {
333  
-                $DB->set_field('gradingform_rubric_fillings', 'levelid', $levelid, $params);
334  
-            }
335  
-        }
336  
-        foreach ($currentgrade as $criterionid => $levelid) {
337  
-            if (!array_key_exists($criterionid, $formdata)) {
338  
-                $params = array('forminstanceid' => $instance->id, 'criterionid' => $criterionid);
339  
-                $DB->delete_records('gradingform_rubric_fillings', $params);
340  
-            }
341  
-        }
342  
-        // TODO: remarks
343  
-        return $this->calculate_grade($formdata, $itemid);
344  
-    }
345  
-
346  
-    /**
347  
-     * Returns html for form element
348  
-     */
349  
-    public function to_html($gradingformelement) {
350  
-        global $PAGE, $USER;
351  
-        if (!$gradingformelement->_flagFrozen) {
352  
-            $module = array('name'=>'gradingform_rubric', 'fullpath'=>'/grade/grading/form/rubric/js/rubric.js');
353  
-            $PAGE->requires->js_init_call('M.gradingform_rubric.init', array(array('name' => $gradingformelement->getName(), 'criteriontemplate' =>'', 'leveltemplate' => '')), true, $module);
354  
-            $mode = self::DISPLAY_EVAL;
355  
-        } else {
356  
-            if ($this->_persistantFreeze) {
357  
-                $mode = gradingform_rubric_controller::DISPLAY_EVAL_FROZEN;
358  
-            } else {
359  
-                $mode = gradingform_rubric_controller::DISPLAY_REVIEW;
360  
-            }
361  
-        }
362  
-        $criteria = $this->definition->rubric_criteria;
363  
-        $submissionid = $gradingformelement->get_grading_attribute('submissionid');
364  
-        $raterid = $USER->id; // TODO - this is very strange!
365  
-        $value = $gradingformelement->getValue();
366  
-        if ($value === null) {
367  
-            $value = $this->get_grading($raterid, $submissionid); // TODO maybe implement in form->set_data() ?
368  
-        }
369  
-        return $this->get_renderer($PAGE)->display_rubric($criteria, $mode, $gradingformelement->getName(), $value);
370  
-    }
371  
-
372  
-    /**
373  
-     * Returns html for form element
374  
-     */
375  
-    public function to_html_old($gradingformelement) {
376  
-        global $PAGE, $USER;
377  
-        //TODO move to renderer
378  
-
379  
-        //$gradingrenderer = $this->prepare_renderer($PAGE);
380  
-        $html = '';
381  
-        $elementname = $gradingformelement->getName();
382  
-        $elementvalue = $gradingformelement->getValue();
383  
-        $submissionid = $gradingformelement->get_grading_attribute('submissionid');
384  
-        $raterid = $USER->id; // TODO - this is very strange!
385  
-        $html .= "assessing submission $submissionid<br />";
386  
-        //$html .= html_writer::empty_tag('input', array('type' => 'text', 'name' => $elementname.'[grade]', 'size' => '20', 'value' => $elementvalue['grade']));
387  
-
388  
-        if (!$gradingformelement->_flagFrozen) {
389  
-            $module = array('name'=>'gradingform_rubric', 'fullpath'=>'/grade/grading/form/rubric/js/rubric.js');
390  
-            $PAGE->requires->js_init_call('M.gradingform_rubric.init', array(array('name' => $gradingformelement->getName(), 'criteriontemplate' =>'', 'leveltemplate' => '')), true, $module);
391  
-        }
392  
-        $criteria = $this->definition->rubric_criteria;
393  
-
394  
-        $html .= html_writer::start_tag('div', array('id' => 'rubric-'.$gradingformelement->getName(), 'class' => 'form_rubric evaluate'));
395  
-        $criteria_cnt = 0;
396  
-
397  
-        $value = $gradingformelement->getValue();
398  
-        if ($value === null) {
399  
-            $value = $this->get_grading($raterid, $submissionid); // TODO maybe implement in form->set_data() ?
400  
-        }
401  
-
402  
-        foreach ($criteria as $criterionid => $criterion) {
403  
-            $html .= html_writer::start_tag('div', array('class' => 'criterion'.$this->get_css_class_suffix($criteria_cnt++, count($criteria)-1)));
404  
-            $html .= html_writer::tag('div', $criterion['description'], array('class' => 'description')); // TODO descriptionformat
405  
-            $html .= html_writer::start_tag('div', array('class' => 'levels'));
406  
-            $level_cnt = 0;
407  
-            foreach ($criterion['levels'] as $levelid => $level) {
408  
-                $checked = (is_array($value) && array_key_exists($criterionid, $value) && ((int)$value[$criterionid] === $levelid));
409  
-                $classsuffix = $this->get_css_class_suffix($level_cnt++, count($criterion['levels'])-1);
410  
-                if ($checked) {
411  
-                    $classsuffix .= ' checked';
412  
-                }
413  
-                $html .= html_writer::start_tag('div', array('id' => $gradingformelement->getName().'-'.$criterionid.'-levels-'.$levelid, 'class' => 'level'.$classsuffix));
414  
-                $input = html_writer::empty_tag('input', array('type' => 'radio', 'name' => $gradingformelement->getName().'['.$criterionid.']', 'value' => $levelid) +
415  
-                    ($checked ? array('checked' => 'checked') : array())); // TODO rewrite
416  
-                $html .= html_writer::tag('div', $input, array('class' => 'radio'));
417  
-                $html .= html_writer::tag('div', $level['definition'], array('class' => 'definition')); // TODO definitionformat
418  
-                $html .= html_writer::tag('div', (float)$level['score'].' pts', array('class' => 'score'));  //TODO span, get_string
419  
-                $html .= html_writer::end_tag('div'); // .level
420  
-            }
421  
-            $html .= html_writer::end_tag('div'); // .levels
422  
-            $html .= html_writer::end_tag('div'); // .criterion
423  
-        }
424  
-        $html .= html_writer::end_tag('div'); // .rubric
425  
-        return $html;
426  
-
427  
-    }
428  
-
429  
-    private function get_css_class_suffix($cnt, $maxcnt) {
430  
-        $class = '';
431  
-        if ($cnt == 0) {
432  
-            $class .= ' first';
433  
-        }
434  
-        if ($cnt == $maxcnt) {
435  
-            $class .= ' last';
436  
-        }
437  
-        if ($cnt%2) {
438  
-            $class .= ' odd';
439  
-        } else {
440  
-            $class .= ' even';
441  
-        }
442  
-        return $class;
443  
-    }
444  
-
445 278
     // TODO the following functions may be moved to parent:
446 279
 
447 280
     /**
@@ -457,7 +290,7 @@ public static function description_form_field_options($context) {
457 290
     }
458 291
 
459 292
     public function get_formatted_description() {
460  
-        if (!$this->definition) {
  293
+        if ($this->definition === false) {
461 294
             return null;
462 295
         }
463 296
         $context = $this->get_context();
@@ -475,53 +308,12 @@ public function get_formatted_description() {
475 308
         return format_text($description, $this->definition->descriptionformat, $formatoptions);
476 309
     }
477 310
 
478  
-    /**
479  
-     * Converts the rubric definition data from the submitted form back to the form
480  
-     * suitable for storing in database
481  
-     */
482  
-    public function postupdate_definition_data($data) {
483  
-        if (!$this->definition) {
484  
-            return $data;
485  
-        }
486  
-        $options = self::description_form_field_options($this->get_context());
487  
-        $data = file_postupdate_standard_editor($data, 'description', $options, $this->get_context(),
488  
-            'gradingform_rubric', 'definition_description', $this->definition->id);
489  
-            // TODO change filearea for embedded files in grading_definition.description
490  
-        return $data;
491  
-    }
492  
-
493 311
     public function is_form_available($foruserid = null) {
494 312
         return true;
495 313
         // TODO this is temporary for testing!
496 314
     }
497 315
 
498 316
     /**
499  
-     * Returns the error message displayed in case of validation failed
500  
-     *
501  
-     * @see validate_grading_element
502  
-     */
503  
-    public function default_validation_error_message() {
504  
-        return 'The rubric is incomplete'; //TODO string
505  
-    }
506  
-
507  
-    /**
508  
-     * Validates that rubric is fully completed and contains valid grade on each criterion
509  
-     */
510  
-    public function validate_grading_element($elementvalue, $itemid) {
511  
-        // TODO: if there is nothing selected in rubric, we don't enter this function at all :(
512  
-        $criteria = $this->definition->rubric_criteria;
513  
-        if (!is_array($elementvalue) || sizeof($elementvalue) < sizeof($criteria)) {
514  
-            return false;
515  
-        }
516  
-        foreach ($criteria as $id => $criterion) {
517  
-            if (!array_key_exists($id, $elementvalue) || !array_key_exists($elementvalue[$id], $criterion['levels'])) {
518  
-                return false;
519  
-            }
520  
-        }
521  
-        return true;
522  
-    }
523  
-
524  
-    /**
525 317
      * Returns the rubric plugin renderer
526 318
      *
527 319
      * @param moodle_page $page the target page
@@ -544,8 +336,8 @@ public function render_preview(moodle_page $page) {
544 336
 
545 337
         // append the rubric itself, using own renderer
546 338
         $output = $this->get_renderer($page);
547  
-        // todo something like $rubric = $output->render_preview($this);
548  
-        $rubric = '[[TODO RUBRIC PREVIEW]]';
  339
+        $criteria = $this->definition->rubric_criteria;
  340
+        $rubric = $output->display_rubric($criteria, self::DISPLAY_PREVIEW, 'rubric');
549 341
 
550 342
         return $header . $rubric;
551 343
     }
@@ -569,4 +361,205 @@ protected function delete_plugin_definition() {
569 361
         // delete critera
570 362
         $DB->delete_records_list('gradingform_rubric_criteria', 'id', $criteria);
571 363
     }
  364
+
  365
+    /**
  366
+     * Returns html code to be included in student's feedback.
  367
+     *
  368
+     * @param moodle_page $page
  369
+     * @param int $itemid
  370
+     * @param array $grading_info result of function grade_get_grades
  371
+     * @param string $defaultcontent default string to be returned if no active grading is found
  372
+     * @return string
  373
+     */
  374
+    public function render_grade($page, $itemid, $grading_info, $defaultcontent) {
  375
+        $instances = $this->get_current_instances($itemid);
  376
+        return $this->get_renderer($page)->display_instances($this->get_current_instances($itemid), $defaultcontent);
  377
+    }
572 378
 }
  379
+
  380
+/**
  381
+ * Class to manage one rubric grading instance. Stores information and performs actions like
  382
+ * update, copy, validate, submit, etc.
  383
+ *
  384
+ * @copyright  2011 Marina Glancy
  385
+ */
  386
+class gradingform_rubric_instance extends gradingform_instance {
  387
+
  388
+    protected $rubric;
  389
+
  390
+    /**
  391
+     * Deletes this (INCOMPLETE) instance from database.
  392
+     */
  393
+    public function cancel() {
  394
+        global $DB;
  395
+        parent::cancel();
  396
+        $DB->delete_records('gradingform_rubric_fillings', array('forminstanceid' => $this->get_id()));
  397
+    }
  398
+
  399
+    /**
  400
+     * Duplicates the instance before editing (optionally substitutes raterid and/or itemid with
  401
+     * the specified values)
  402
+     *
  403
+     * @param int $raterid value for raterid in the duplicate
  404
+     * @param int $itemid value for itemid in the duplicate
  405
+     * @return int id of the new instance
  406
+     */
  407
+    public function copy($raterid, $itemid) {
  408
+        global $DB;
  409
+        $instanceid = parent::copy($raterid, $itemid);
  410
+        $currentgrade = $this->get_rubric_filling();
  411
+        foreach ($currentgrade['criteria'] as $criterionid => $record) {
  412
+            $params = array('forminstanceid' => $instanceid, 'criterionid' => $criterionid,
  413
+                'levelid' => $record['levelid'], 'remark' => $record['remark'], 'remarkformat' => $record['remarkformat']);
  414
+            $DB->insert_record('gradingform_rubric_fillings', $params);
  415
+        }
  416
+        return $instanceid;
  417
+    }
  418
+
  419
+    /**
  420
+     * Validates that rubric is fully completed and contains valid grade on each criterion
  421
+     * @return boolean true if the form data is validated and contains no errors
  422
+     */
  423
+    public function validate_grading_element($elementvalue) {
  424
+        // TODO: if there is nothing selected in rubric, we don't enter this function at all :(
  425
+        $criteria = $this->get_controller()->get_definition()->rubric_criteria;
  426
+        if (!isset($elementvalue['criteria']) || !is_array($elementvalue['criteria']) || sizeof($elementvalue['criteria']) < sizeof($criteria)) {
  427
+            return false;
  428
+        }
  429
+        foreach ($criteria as $id => $criterion) {
  430
+            if (!isset($elementvalue['criteria'][$id]['levelid'])
  431
+                    || !array_key_exists($elementvalue['criteria'][$id]['levelid'], $criterion['levels'])) {
  432
+                return false;
  433
+            }
  434
+        }
  435
+        return true;
  436
+    }
  437
+
  438
+    /**
  439
+     * Retrieves from DB and returns the data how this rubric was filled
  440
+     *
  441
+     * @param boolean $force whether to force DB query even if the data is cached
  442
+     * @return array
  443
+     */
  444
+    public function get_rubric_filling($force = false) {
  445
+        global $DB;
  446
+        if ($this->rubric === null || $force) {
  447
+            $records = $DB->get_records('gradingform_rubric_fillings', array('forminstanceid' => $this->get_id()));
  448
+            $this->rubric = array('criteria' => array());
  449
+            foreach ($records as $record) {
  450
+                $this->rubric['criteria'][$record->criterionid] = (array)$record;
  451
+            }
  452
+        }
  453
+        return $this->rubric;
  454
+    }
  455
+
  456
+    /**
  457
+     * Updates the instance with the data received from grading form. This function may be
  458
+     * called via AJAX when grading is not yet completed, so it does not change the
  459
+     * status of the instance.
  460
+     *
  461
+     * @param array $data
  462
+     */
  463
+    public function update($data) {
  464
+        global $DB;
  465
+        $currentgrade = $this->get_rubric_filling();
  466
+        parent::update($data);
  467
+        foreach ($data['criteria'] as $criterionid => $record) {
  468
+            if (!array_key_exists($criterionid, $currentgrade['criteria'])) {
  469
+                $newrecord = array('forminstanceid' => $this->get_id(), 'criterionid' => $criterionid,
  470
+                    'levelid' => $record['levelid'], 'remark' => $record['remark'], 'remarkformat' => FORMAT_MOODLE);
  471
+                $DB->insert_record('gradingform_rubric_fillings', $newrecord);
  472
+            } else {
  473
+                $newrecord = array('id' => $currentgrade['criteria'][$criterionid]['id']);
  474
+                foreach (array('levelid', 'remark'/*, 'remarkformat' TODO */) as $key) {
  475
+                    if ($currentgrade['criteria'][$criterionid][$key] != $record[$key]) {
  476
+                        $newrecord[$key] = $record[$key];
  477
+                    }
  478
+                }
  479
+                if (count($newrecord) > 1) {
  480
+                    $DB->update_record('gradingform_rubric_fillings', $newrecord);
  481
+                }
  482
+            }
  483
+        }
  484
+        foreach ($currentgrade['criteria'] as $criterionid => $record) {
  485
+            if (!array_key_exists($criterionid, $data['criteria'])) {
  486
+                $DB->delete_records('gradingform_rubric_fillings', array('id' => $record['id']));
  487
+            }
  488
+        }
  489
+        $this->get_rubric_filling(true);
  490
+    }
  491
+
  492
+    /**
  493
+     * Calculates the grade to be pushed to the gradebook
  494
+     *
  495
+     * @return int the valid grade from $this->get_controller()->get_grade_range()
  496
+     */
  497
+    public function get_grade() {
  498
+        global $DB, $USER;
  499
+        $grade = $this->get_rubric_filling();
  500
+
  501
+        $minscore = 0;
  502
+        $maxscore = 0;
  503
+        foreach ($this->get_controller()->get_definition()->rubric_criteria as $id => $criterion) {
  504
+            $keys = array_keys($criterion['levels']);
  505
+            sort($keys);
  506
+            $minscore += $criterion['levels'][$keys[0]]['score'];
  507
+            $maxscore += $criterion['levels'][$keys[sizeof($keys)-1]]['score'];
  508
+        }
  509
+
  510
+        if ($maxscore <= $minscore) {
  511
+            return -1;
  512
+        }
  513
+
  514
+        $graderange = array_keys($this->get_controller()->get_grade_range());
  515
+        if (empty($graderange)) {
  516
+            return -1;
  517
+        }
  518
+        sort($graderange);
  519
+        $mingrade = $graderange[0];
  520
+        $maxgrade = $graderange[sizeof($graderange) - 1];
  521
+
  522
+        $curscore = 0;
  523
+        foreach ($grade['criteria'] as $id => $record) {
  524
+            $curscore += $this->get_controller()->get_definition()->rubric_criteria[$id]['levels'][$record['levelid']]['score'];
  525
+        }
  526
+        return round(($curscore-$minscore)/($maxscore-$minscore)*($maxgrade-$mingrade), 0) + $mingrade; // TODO mapping
  527
+    }
  528
+
  529
+    /**
  530
+     * Returns the error message displayed in case of validation failed
  531
+     *
  532
+     * @return string
  533
+     */
  534
+    public function default_validation_error_message() {
  535
+        return 'The rubric is incomplete'; //TODO string
  536
+    }
  537
+
  538
+    /**
  539
+     * Returns html for form element of type 'grading'.
  540
+     *
  541
+     * @param moodle_page $page
  542
+     * @param MoodleQuickForm_grading $formelement
  543
+     * @return string
  544
+     */
  545
+    public function render_grading_element($page, $gradingformelement) {
  546
+        global $USER;
  547
+        if (!$gradingformelement->_flagFrozen) {
  548
+            $module = array('name'=>'gradingform_rubric', 'fullpath'=>'/grade/grading/form/rubric/js/rubric.js');
  549
+            $page->requires->js_init_call('M.gradingform_rubric.init', array(array('name' => $gradingformelement->getName())), true, $module);
  550
+            $mode = gradingform_rubric_controller::DISPLAY_EVAL;
  551
+        } else {
  552
+            if ($gradingformelement->_persistantFreeze) {
  553
+                $mode = gradingform_rubric_controller::DISPLAY_EVAL_FROZEN;
  554
+            } else {
  555
+                $mode = gradingform_rubric_controller::DISPLAY_REVIEW;
  556
+            }
  557
+        }
  558
+        $criteria = $this->get_controller()->get_definition()->rubric_criteria;
  559
+        $value = $gradingformelement->getValue();
  560
+        if ($value === null) {
  561
+            $value = $this->get_rubric_filling();
  562
+        }
  563
+        return $this->get_controller()->get_renderer($page)->display_rubric($criteria, $mode, $gradingformelement->getName(), $value);
  564
+    }
  565
+}
65  grade/grading/form/rubric/renderer.php
@@ -35,8 +35,8 @@ class gradingform_rubric_renderer {
35 35
      * @param int $mode @see gradingform_rubric_controller
36 36
      * @return string
37 37
      */
38  
-    public function criterion_template($mode, $elementname = '{NAME}', $criterion = null, $levels_str = '{LEVELS}') {
39  
-        // TODO description format
  38
+    public function criterion_template($mode, $elementname = '{NAME}', $criterion = null, $levels_str = '{LEVELS}', $value = null) {
  39
+        // TODO description format, remark format
40 40
         if ($criterion === null || !is_array($criterion) || !array_key_exists('id', $criterion)) {
41 41
             $criterion = array('id' => '{CRITERION-id}', 'description' => '{CRITERION-description}', 'sortorder' => '{CRITERION-sortorder}', 'class' => '{CRITERION-class}');
42 42
         } else {
@@ -74,6 +74,21 @@ public function criterion_template($mode, $elementname = '{NAME}', $criterion =
74 74
                 'id' => '{NAME}-{CRITERION-id}-levels-addlevel', 'value' => $value, 'title' => $value)); //TODO '{NAME}-{CRITERION-id}-levels-addlevel
75 75
             $criterion_template .= html_writer::tag('div', $button, array('class' => 'addlevel'));
76 76
         }
  77
+        if (isset($value['remark'])) {
  78
+            $currentremark = $value['remark'];
  79
+        } else {
  80
+            $currentremark = '';
  81
+        }
  82
+        if ($mode == gradingform_rubric_controller::DISPLAY_EVAL) {
  83
+            $input = html_writer::tag('textarea', htmlspecialchars($currentremark), array('name' => '{NAME}[criteria][{CRITERION-id}][remark]', 'cols' => '10', 'rows' => '5'));
  84
+            $criterion_template .= html_writer::tag('div', $input, array('class' => 'remark'));
  85
+        }
  86
+        if ($mode == gradingform_rubric_controller::DISPLAY_EVAL_FROZEN) {
  87
+            $criterion_template .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[criteria][{CRITERION-id}][remark]', 'value' => $currentremark));
  88
+        }
  89
+        if ($mode == gradingform_rubric_controller::DISPLAY_REVIEW) {
  90
+            $criterion_template .= html_writer::tag('div', $currentremark, array('class' => 'remark')); // TODO maybe some prefix here like 'Teacher remark:'
  91
+        }
77 92
         $criterion_template .= html_writer::end_tag('div'); // .criterion
78 93
 
79 94
         $criterion_template = str_replace('{NAME}', $elementname, $criterion_template);
@@ -83,7 +98,7 @@ public function criterion_template($mode, $elementname = '{NAME}', $criterion =
83 98
 
84 99
     public function level_template($mode, $elementname = '{NAME}', $criterionid = '{CRITERION-id}', $level = null) {
85 100
         // TODO definition format
86  
-        if ($level === null || !is_array($level) || !array_key_exists('id', $level)) {
  101
+        if (!isset($level['id'])) {
87 102
             $level = array('id' => '{LEVEL-id}', 'definition' => '{LEVEL-definition}', 'score' => '{LEVEL-score}', 'class' => '{LEVEL-class}', 'checked' => false);
88 103
         } else {
89 104
             foreach (array('score', 'definition', 'class', 'checked') as $key) {
@@ -108,12 +123,12 @@ public function level_template($mode, $elementname = '{NAME}', $criterionid = '{
108 123
             $score = $level['score'];
109 124
         }
110 125
         if ($mode == gradingform_rubric_controller::DISPLAY_EVAL) {
111  
-            $input = html_writer::empty_tag('input', array('type' => 'radio', 'name' => '{NAME}[{CRITERION-id}]', 'value' => $level['id']) +
  126
+            $input = html_writer::empty_tag('input', array('type' => 'radio', 'name' => '{NAME}[criteria][{CRITERION-id}][levelid]', 'value' => $level['id']) +
112 127
                     ($level['checked'] ? array('checked' => 'checked') : array()));
113 128
             $level_template .= html_writer::tag('div', $input, array('class' => 'radio'));
114 129
         }
115 130
         if ($mode == gradingform_rubric_controller::DISPLAY_EVAL_FROZEN && $level['checked']) {
116  
-            $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[{CRITERION-id}]', 'value' => $level['id']));
  131
+            $level_template .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[criteria][{CRITERION-id}][levelid]', 'value' => $level['id']));
117 132
         }
118 133
         $score = html_writer::tag('span', $score, array('id' => '{NAME}-{CRITERION-id}-levels-{LEVEL-id}-score'));
119 134
         $level_template .= html_writer::tag('div', $definition, array('class' => 'definition', 'id' => '{NAME}-{CRITERION-id}-levels-{LEVEL-id}-definition'));
@@ -176,17 +191,22 @@ public function display_rubric($criteria, $mode, $elementname = null, $values =
176 191
             $criterion['class'] = $this->get_css_class_suffix($cnt++, sizeof($criteria) -1);
177 192
             $levels_str = '';
178 193
             $levelcnt = 0;
  194
+            if (isset($values['criteria'][$id])) {
  195
+                $criterionvalue = $values['criteria'][$id];
  196
+            } else {
  197
+                $criterionvalue = null;
  198
+            }
179 199
             foreach ($criterion['levels'] as $levelid => $level) {
180 200
                 $level['score'] = (float)$level['score']; // otherwise the display will look like 1.00000
181 201
                 $level['class'] = $this->get_css_class_suffix($levelcnt++, sizeof($criterion['levels']) -1);
182  
-                $level['checked'] = (is_array($values) && (array_key_exists($id, $values) && ((int)$values[$id] === $levelid)));
  202
+                $level['checked'] = (isset($criterionvalue['levelid']) && ((int)$criterionvalue['levelid'] === $levelid));
183 203
                 if ($level['checked'] && ($mode == gradingform_rubric_controller::DISPLAY_EVAL_FROZEN || $mode == gradingform_rubric_controller::DISPLAY_REVIEW)) {
184 204
                     $level['class'] .= ' checked';
185 205
                     //in mode DISPLAY_EVAL the class 'checked' will be added by JS if it is enabled. If it is not enabled, the 'checked' class will only confuse
186 206
                 }
187 207
                 $levels_str .= $this->level_template($mode, $elementname, $id, $level);
188 208
             }
189  
-            $criteria_str .= $this->criterion_template($mode, $elementname, $criterion, $levels_str);
  209
+            $criteria_str .= $this->criterion_template($mode, $elementname, $criterion, $levels_str, $criterionvalue);
190 210
         }
191 211
         return $this->rubric_template($mode, $elementname, $criteria_str);
192 212
     }
@@ -213,4 +233,35 @@ private function get_css_class_suffix($cnt, $maxcnt) {
213 233
         }
214 234
         return $class;
215 235
     }
  236
+
  237
+    /**
  238
+     * Displays for the student the list of instances or default content if no instances found
  239
+     *
  240
+     * @param array $instances array of objects of type gradingform_rubric_instance
  241
+     * @param string $defaultcontent default string that would be displayed without advanced grading
  242
+     * @return string
  243
+     */
  244
+    public function display_instances($instances, $defaultcontent) {
  245
+        if (sizeof($instances)) {
  246
+            $rv = html_writer::start_tag('div', array('class' => 'advancedgrade'));
  247
+            $idx = 0;
  248
+            foreach ($instances as $instance) {
  249
+                $rv .= $this->display_instance($instance, $idx++);
  250
+            }
  251
+            $rv .= html_writer::end_tag('div');
  252
+        }
  253
+        return $rv. $defaultcontent;
  254
+    }
  255
+
  256
+    /**
  257
+     * Displays one grading instance
  258
+     *
  259
+     * @param gradingform_rubric_instance $instance
  260
+     * @param int idx unique number of instance on page
  261
+     */
  262
+    public function display_instance(gradingform_rubric_instance $instance, $idx) {
  263
+        $criteria = $instance->get_controller()->get_definition()->rubric_criteria;
  264
+        $values = $instance->get_rubric_filling();
  265
+        return $this->display_rubric($criteria, gradingform_rubric_controller::DISPLAY_REVIEW, 'rubric'.$idx, $values);
  266
+    }
216 267
 }
6  grade/grading/form/rubric/styles.css
@@ -20,6 +20,8 @@
20 20
                     [input type=submit]
21 21
         .addlevel
22 22
             [input type=submit]
  23
+        .remark
  24
+            textarea