diff --git a/grade/grading/form/lib.php b/grade/grading/form/lib.php index f8e3b6100da..641baf394ce 100644 --- a/grade/grading/form/lib.php +++ b/grade/grading/form/lib.php @@ -287,65 +287,92 @@ abstract class gradingform_controller { } /** - * Makes sure there is a form instance for the given rater grading the given item + * Returns the ACTIVE instance for this definition for the specified $raterid and $itemid + * (if multiple raters are allowed, or only for $itemid otherwise). * - * Plugins will probably override/extend this and load additional data of how their - * forms are filled in one complex query. - * - * @todo this might actually become abstract method * @param int $raterid * @param int $itemid - * @return stdClass newly created or existing record from {grading_instances} + * @param boolean $idonly + * @return mixed if $idonly=true returns id of the found instance, otherwise returns the instance object */ - public function prepare_instance($raterid, $itemid) { + public function get_current_instance($raterid, $itemid, $idonly = false) { global $DB; - - if (empty($this->definition)) { - throw new coding_exception('Attempting to prepare an instance of non-existing grading form'); + $select = array( + 'formid' => $this->definition->id, + 'itemid' => $itemid, + 'status' => gradingform_instance::INSTANCE_STATUS_ACTIVE); + if (false /* TODO $manager->allow_multiple_raters() */) { + $select['raterid'] = $raterid; } - - $current = $DB->get_record('grading_instances', array( - 'formid' => $this->definition->id, - 'raterid' => $raterid, - 'itemid' => $itemid), '*', IGNORE_MISSING); - - if (empty($current)) { - $instance = new stdClass(); - $instance->formid = $this->definition->id; - $instance->raterid = $raterid; - $instance->itemid = $itemid; - $instance->timemodified = time(); - $instance->feedbackformat = FORMAT_MOODLE; - $instance->id = $DB->insert_record('grading_instances', $instance); - return $instance; - + if ($idonly) { + if ($current = $DB->get_record('grading_instances', $select, 'id', IGNORE_MISSING)) { + return $current->id; + } } else { - return $current; + if ($current = $DB->get_record('grading_instances', $select, '*', IGNORE_MISSING)) { + return $this->get_instance($current); + } } + return null; } /** - * Saves non-js data and returns the gradebook grade - */ - abstract public function save_and_get_grade($raterid, $itemid, $formdata); - - /** - * Returns html for form element - */ - abstract public function to_html($gradingformelement); - - /** + * Returns list of active instances for the specified $itemid * + * @param int $itemid + * @return array of gradingform_instance objects */ - public function default_validation_error_message() { - return ''; + public function get_current_instances($itemid) { + global $DB; + $conditions = array('formid' => $this->definition->id, + 'itemid' => $itemid, + 'status' => gradingform_instance::INSTANCE_STATUS_ACTIVE); + $records = $DB->get_recordset('grading_instances', $conditions); + $rv = array(); + foreach ($records as $record) { + $rv[] = $this->get_instance($record); + } + return $rv; } /** + * Returns the object of type gradingform_XXX_instance (where XXX is the plugin method name) * + * @param mixed $instance id or row from grading_isntances table + * @return gradingform_instance */ - public function validate_grading_element($elementvalue, $itemid) { - return true; + protected function get_instance($instance) { + global $DB; + if (is_scalar($instance)) { + // instance id is passed as parameter + $instance = $DB->get_record('grading_instances', array('id' => $instance), '*', MUST_EXIST); + } + if ($instance) { + $class = 'gradingform_'. $this->get_method_name(). '_instance'; + return new $class($this, $instance); + } + return null; + } + + /** + * This function is invoked when user (teacher) starts grading. + * It creates and returns copy of the current ACTIVE instance if it exists. If this is the + * first grading attempt, a new instance is created. + * The status of the returned instance is INCOMPLETE + * + * @param int $raterid + * @param int $itemid + * @return gradingform_instance + */ + public function create_instance($raterid, $itemid) { + global $DB; + // first find if there is already an active instance for this itemid + if ($current = $this->get_current_instance($raterid, $itemid)) { + return $this->get_instance($current->copy($raterid, $itemid)); + } else { + $class = 'gradingform_'. $this->get_method_name(). '_instance'; + return $this->get_instance($class::create_new($this->definition->id, $raterid, $itemid)); + } } /** @@ -426,4 +453,225 @@ abstract class gradingform_controller { throw new coding_exception('Invalid class name'); } } + + /** + * Returns html code to be included in student's feedback. + * + * @param moodle_page $page + * @param int $itemid + * @param string $defaultcontent default string to be returned if no active grading is found + * @return string + */ + public function render_grade($page, $itemid, $defaultcontent) { + return $defaultcontent; + } } + +/** + * Class to manage one grading instance. Stores information and performs actions like + * update, copy, validate, submit, etc. + * + * @copyright 2011 Marina Glancy + */ +abstract class gradingform_instance { + const INSTANCE_STATUS_ACTIVE = 1; + const INSTANCE_STATUS_INCOMPLETE = 0; + const INSTANCE_STATUS_ARCHIVE = 3; + + /** @var stdClass record from table grading_instances */ + protected $data; + /** @var gradingform_controller link to the corresponding controller */ + protected $controller; + + /** + * Creates an instance + * + * @param gradingform_controller $controller + * @param stdClass $data + */ + public function __construct($controller, $data) { + $this->data = (object)$data; + $this->controller = $controller; + } + + /** + * Creates a new empty instance in DB and mark its status as INCOMPLETE + * + * @param int $formid + * @param int $raterid + * @param int $itemid + * @return int id of the created instance + */ + public static function create_new($formid, $raterid, $itemid) { + global $DB; + $instance = new stdClass(); + $instance->formid = $formid; + $instance->raterid = $raterid; + $instance->itemid = $itemid; + $instance->status = self::INSTANCE_STATUS_INCOMPLETE; + $instance->timemodified = time(); + $instance->feedbackformat = FORMAT_MOODLE; + $instanceid = $DB->insert_record('grading_instances', $instance); + return $instanceid; + } + + /** + * Duplicates the instance before editing (optionally substitutes raterid and/or itemid with + * the specified values) + * Plugins may want to override this function to copy data from additional tables as well + * + * @param int $raterid value for raterid in the duplicate + * @param int $itemid value for itemid in the duplicate + * @return int id of the new instance + */ + public function copy($raterid, $itemid) { + global $DB; + $data = (array)$this->data; // Cast to array to make a copy + unset($data['id']); + $data['raterid'] = $raterid; + $data['itemid'] = $itemid; + $data['timemodified'] = time(); + $data['status'] = self::INSTANCE_STATUS_INCOMPLETE; + $instanceid = $DB->insert_record('grading_instances', $data); + return $instanceid; + } + + /** + * Returns the controller + * + * @return gradingform_controller + */ + public function get_controller() { + return $this->controller; + } + + /** + * Returns instance id + * + * @return int + */ + public function get_id() { + return $this->data->id; + } + + /** + * Marks the instance as ACTIVE and current active instance (if exists) as ARCHIVE + */ + protected function make_active() { + global $DB; + if ($this->data->status == self::INSTANCE_STATUS_ACTIVE) { + // already active + return; + } + $currentid = $this->get_controller()->get_current_instance($this->data->raterid, $this->data->itemid, true); + if ($currentid) { + if ($currentid != $this->get_id()) { + $DB->update_record('grading_instances', array('id' => $currentid, 'status' => self::INSTANCE_STATUS_ARCHIVE)); + $DB->update_record('grading_instances', array('id' => $this->get_id(), 'status' => self::INSTANCE_STATUS_ACTIVE)); + } + } else { + $DB->update_record('grading_instances', array('id' => $this->get_id(), 'status' => self::INSTANCE_STATUS_ACTIVE)); + } + $this->data->status = self::INSTANCE_STATUS_ACTIVE; + } + + /** + * Deletes this (INCOMPLETE) instance from database. This function is invoked on cancelling the + * grading form and/or during cron cleanup. + * Plugins using additional tables must override this method to remove additional data. + * Note that if the teacher just closes the window or presses 'Back' button of the browser, + * this function is not invoked. + */ + public function cancel() { + global $DB; + // TODO what if we happen delete the ACTIVE instance, shall we rollback to the last ARCHIVE? or throw an exception? + // TODO create cleanup cron + $DB->delete_records('grading_instances', array('id' => $this->get_id())); + } + + /** + * Updates the instance with the data received from grading form. This function may be + * called via AJAX when grading is not yet completed, so it does not change the + * status of the instance. + */ + public function update($elementvalue) { + // TODO update timemodified at least + } + + /** + * Calculates the grade to be pushed to the gradebook + * @return int the grade on 0-100 scale + */ + abstract public function get_grade(); + + /** + * Called when teacher submits the grading form: + * updates the instance in DB, marks it as ACTIVE and returns the grade to be pushed to the gradebook + * @return int the grade on 0-100 scale + */ + public function submit_and_get_grade($elementvalue) { + $this->update($elementvalue); + $this->make_active(); + return $this->get_grade(); + } + + + /** + * Returns html for form element of type 'grading'. If there is a form input element + * it must have the name $gradingformelement->getName(). + * If there are more than one input elements they MUST be elements of array with + * name $gradingformelement->getName(). + * Example: {NAME}[myelement1], {NAME}[myelement2][sub1], {NAME}[myelement2][sub2], etc. + * After submitting the form the value of $_POST[{NAME}] is passed to the functions + * validate_grading_element() and submit_and_get_grade() + * + * Plugins may use $gradingformelement->getValue() to get the value passed on previous + * from submit + * + * When forming html it is a plugin's responsibility to analyze flags + * $gradingformelement->_flagFrozen and $gradingformelement->_persistantFreeze: + * + * (_flagFrozen == false) => form element is editable + * + * (_flagFrozen == false && _persistantFreeze == true) => form element is not editable + * but all values are passed as hidden elements + * + * (_flagFrozen == false && _persistantFreeze == false) => form element is not editable + * and no values are passed as hidden elements + * + * Plugins are welcome to use AJAX in the form element. But it is strongly recommended + * that the grading only becomes active when teacher presses 'Submit' button (the + * method submit_and_get_grade() is invoked) + * + * Also client-side JS validation may be implemented here + * + * @see MoodleQuickForm_grading in lib/form/grading.php + * + * @param moodle_page $page + * @param MoodleQuickForm_grading $gradingformelement + * @return string + */ + abstract function render_grading_element($page, $gradingformelement); + + /** + * Server-side validation of the data received from grading form. + * + * @param mixed $elementvalue is the scalar or array received in $_POST + * @return boolean true if the form data is validated and contains no errors + */ + public function validate_grading_element($elementvalue) { + return true; + } + + /** + * Returns the error message displayed if validation failed. + * If plugin wants to display custom message, the empty string should be returned here + * and the custom message should be output in render_grading_element() + * + * @see validate_grading_element() + * @return string + */ + public function default_validation_error_message() { + return ''; + } +} \ No newline at end of file diff --git a/grade/grading/form/rubric/lib.php b/grade/grading/form/rubric/lib.php index 763b94620fe..a12a55c63cf 100644 --- a/grade/grading/form/rubric/lib.php +++ b/grade/grading/form/rubric/lib.php @@ -266,182 +266,6 @@ class gradingform_rubric_controller extends gradingform_controller { return $new; } - public function get_grading($raterid, $itemid) { - global $DB; - $sql = "SELECT f.id, f.criterionid, f.levelid, f.remark, f.remarkformat - FROM {grading_instances} i, {gradingform_rubric_fillings} f - WHERE i.formid = :formid ". - "AND i.raterid = :raterid ". - "AND i.itemid = :itemid - AND i.id = f.forminstanceid"; - $params = array('formid' => $this->definition->id, 'itemid' => $itemid, 'raterid' => $raterid); - $rs = $DB->get_recordset_sql($sql, $params); - $grading = array(); - foreach ($rs as $record) { - if ($record->levelid) { - $grading[$record->criterionid] = $record->levelid; - } - // TODO: remarks - } - $rs->close(); - return $grading; - } - - /** - * Converts the rubric data to the gradebook score 0-100 - */ - protected function calculate_grade($grade, $itemid) { - if (!$this->validate_grading_element($grade, $itemid)) { - return -1; - } - - $minscore = 0; - $maxscore = 0; - foreach ($this->definition->rubric_criteria as $id => $criterion) { - $keys = array_keys($criterion['levels']); - // TODO array_reverse($keys) if levels are sorted DESC - $minscore += $criterion['levels'][$keys[0]]['score']; - $maxscore += $criterion['levels'][$keys[sizeof($keys)-1]]['score']; - } - - if ($maxscore == 0) { - return -1; - } - - $curscore = 0; - foreach ($grade as $id => $levelid) { - $curscore += $this->definition->rubric_criteria[$id]['levels'][$levelid]['score']; - } - return $curscore/$maxscore*100; // TODO mapping - } - - /** - * Saves non-js data and returns the gradebook grade - */ - public function save_and_get_grade($raterid, $itemid, $formdata) { - global $DB, $USER; - $instance = $this->prepare_instance($raterid, $itemid); - $currentgrade = $this->get_grading($raterid, $itemid); - if (!is_array($formdata)) { - return $this->calculate_grade($currentgrade, $itemid); - } - foreach ($formdata as $criterionid => $levelid) { - $params = array('forminstanceid' => $instance->id, 'criterionid' => $criterionid); - if (!array_key_exists($criterionid, $currentgrade)) { - $DB->insert_record('gradingform_rubric_fillings', $params + array('levelid' => $levelid)); - } else if ($currentgrade[$criterionid] != $levelid) { - $DB->set_field('gradingform_rubric_fillings', 'levelid', $levelid, $params); - } - } - foreach ($currentgrade as $criterionid => $levelid) { - if (!array_key_exists($criterionid, $formdata)) { - $params = array('forminstanceid' => $instance->id, 'criterionid' => $criterionid); - $DB->delete_records('gradingform_rubric_fillings', $params); - } - } - // TODO: remarks - return $this->calculate_grade($formdata, $itemid); - } - - /** - * Returns html for form element - */ - public function to_html($gradingformelement) { - global $PAGE, $USER; - if (!$gradingformelement->_flagFrozen) { - $module = array('name'=>'gradingform_rubric', 'fullpath'=>'/grade/grading/form/rubric/js/rubric.js'); - $PAGE->requires->js_init_call('M.gradingform_rubric.init', array(array('name' => $gradingformelement->getName(), 'criteriontemplate' =>'', 'leveltemplate' => '')), true, $module); - $mode = self::DISPLAY_EVAL; - } else { - if ($this->_persistantFreeze) { - $mode = gradingform_rubric_controller::DISPLAY_EVAL_FROZEN; - } else { - $mode = gradingform_rubric_controller::DISPLAY_REVIEW; - } - } - $criteria = $this->definition->rubric_criteria; - $submissionid = $gradingformelement->get_grading_attribute('submissionid'); - $raterid = $USER->id; // TODO - this is very strange! - $value = $gradingformelement->getValue(); - if ($value === null) { - $value = $this->get_grading($raterid, $submissionid); // TODO maybe implement in form->set_data() ? - } - return $this->get_renderer($PAGE)->display_rubric($criteria, $mode, $gradingformelement->getName(), $value); - } - - /** - * Returns html for form element - */ - public function to_html_old($gradingformelement) { - global $PAGE, $USER; - //TODO move to renderer - - //$gradingrenderer = $this->prepare_renderer($PAGE); - $html = ''; - $elementname = $gradingformelement->getName(); - $elementvalue = $gradingformelement->getValue(); - $submissionid = $gradingformelement->get_grading_attribute('submissionid'); - $raterid = $USER->id; // TODO - this is very strange! - $html .= "assessing submission $submissionid
"; - //$html .= html_writer::empty_tag('input', array('type' => 'text', 'name' => $elementname.'[grade]', 'size' => '20', 'value' => $elementvalue['grade'])); - - if (!$gradingformelement->_flagFrozen) { - $module = array('name'=>'gradingform_rubric', 'fullpath'=>'/grade/grading/form/rubric/js/rubric.js'); - $PAGE->requires->js_init_call('M.gradingform_rubric.init', array(array('name' => $gradingformelement->getName(), 'criteriontemplate' =>'', 'leveltemplate' => '')), true, $module); - } - $criteria = $this->definition->rubric_criteria; - - $html .= html_writer::start_tag('div', array('id' => 'rubric-'.$gradingformelement->getName(), 'class' => 'form_rubric evaluate')); - $criteria_cnt = 0; - - $value = $gradingformelement->getValue(); - if ($value === null) { - $value = $this->get_grading($raterid, $submissionid); // TODO maybe implement in form->set_data() ? - } - - foreach ($criteria as $criterionid => $criterion) { - $html .= html_writer::start_tag('div', array('class' => 'criterion'.$this->get_css_class_suffix($criteria_cnt++, count($criteria)-1))); - $html .= html_writer::tag('div', $criterion['description'], array('class' => 'description')); // TODO descriptionformat - $html .= html_writer::start_tag('div', array('class' => 'levels')); - $level_cnt = 0; - foreach ($criterion['levels'] as $levelid => $level) { - $checked = (is_array($value) && array_key_exists($criterionid, $value) && ((int)$value[$criterionid] === $levelid)); - $classsuffix = $this->get_css_class_suffix($level_cnt++, count($criterion['levels'])-1); - if ($checked) { - $classsuffix .= ' checked'; - } - $html .= html_writer::start_tag('div', array('id' => $gradingformelement->getName().'-'.$criterionid.'-levels-'.$levelid, 'class' => 'level'.$classsuffix)); - $input = html_writer::empty_tag('input', array('type' => 'radio', 'name' => $gradingformelement->getName().'['.$criterionid.']', 'value' => $levelid) + - ($checked ? array('checked' => 'checked') : array())); // TODO rewrite - $html .= html_writer::tag('div', $input, array('class' => 'radio')); - $html .= html_writer::tag('div', $level['definition'], array('class' => 'definition')); // TODO definitionformat - $html .= html_writer::tag('div', (float)$level['score'].' pts', array('class' => 'score')); //TODO span, get_string - $html .= html_writer::end_tag('div'); // .level - } - $html .= html_writer::end_tag('div'); // .levels - $html .= html_writer::end_tag('div'); // .criterion - } - $html .= html_writer::end_tag('div'); // .rubric - return $html; - - } - - private function get_css_class_suffix($cnt, $maxcnt) { - $class = ''; - if ($cnt == 0) { - $class .= ' first'; - } - if ($cnt == $maxcnt) { - $class .= ' last'; - } - if ($cnt%2) { - $class .= ' odd'; - } else { - $class .= ' even'; - } - return $class; - } - // TODO the following functions may be moved to parent: /** @@ -495,32 +319,6 @@ class gradingform_rubric_controller extends gradingform_controller { // TODO this is temporary for testing! } - /** - * Returns the error message displayed in case of validation failed - * - * @see validate_grading_element - */ - public function default_validation_error_message() { - return 'The rubric is incomplete'; //TODO string - } - - /** - * Validates that rubric is fully completed and contains valid grade on each criterion - */ - public function validate_grading_element($elementvalue, $itemid) { - // TODO: if there is nothing selected in rubric, we don't enter this function at all :( - $criteria = $this->definition->rubric_criteria; - if (!is_array($elementvalue) || sizeof($elementvalue) < sizeof($criteria)) { - return false; - } - foreach ($criteria as $id => $criterion) { - if (!array_key_exists($id, $elementvalue) || !array_key_exists($elementvalue[$id], $criterion['levels'])) { - return false; - } - } - return true; - } - /** * Returns the rubric plugin renderer * @@ -544,8 +342,8 @@ class gradingform_rubric_controller extends gradingform_controller { // append the rubric itself, using own renderer $output = $this->get_renderer($page); - // todo something like $rubric = $output->render_preview($this); - $rubric = '[[TODO RUBRIC PREVIEW]]'; + $criteria = $this->definition->rubric_criteria; + $rubric = $output->display_rubric($criteria, self::DISPLAY_PREVIEW, 'rubric'); return $header . $rubric; } @@ -569,4 +367,183 @@ class gradingform_rubric_controller extends gradingform_controller { // delete critera $DB->delete_records_list('gradingform_rubric_criteria', 'id', $criteria); } + + /** + * Returns html code to be included in student's feedback. + * + * @param moodle_page $page + * @param int $itemid + * @param string $defaultcontent default string to be returned if no active grading is found + * @return string + */ + public function render_grade($page, $itemid, $defaultcontent) { + $instances = $this->get_current_instances($itemid); + return $this->get_renderer($page)->display_instances($this->get_current_instances($itemid), $defaultcontent); + } } + +/** + * Class to manage one rubric grading instance. Stores information and performs actions like + * update, copy, validate, submit, etc. + * + * @copyright 2011 Marina Glancy + */ +class gradingform_rubric_instance extends gradingform_instance { + + /** + * Deletes this (INCOMPLETE) instance from database. + */ + public function cancel() { + global $DB; + parent::cancel(); + $DB->delete_records('gradingform_rubric_fillings', array('forminstanceid' => $this->get_id())); + } + + /** + * Duplicates the instance before editing (optionally substitutes raterid and/or itemid with + * the specified values) + * + * @param int $raterid value for raterid in the duplicate + * @param int $itemid value for itemid in the duplicate + * @return int id of the new instance + */ + public function copy($raterid, $itemid) { + global $DB; + $instanceid = parent::copy($raterid, $itemid); + $currentgrade = $this->get_rubric_filling(); + foreach ($currentgrade as $criterionid => $levelid) { + $params = array('forminstanceid' => $instanceid, 'criterionid' => $criterionid, 'levelid' => $levelid); + $DB->insert_record('gradingform_rubric_fillings', $params); + } + // TODO remarks + return $instanceid; + } + + /** + * Validates that rubric is fully completed and contains valid grade on each criterion + * @return boolean true if the form data is validated and contains no errors + */ + public function validate_grading_element($elementvalue) { + // TODO: if there is nothing selected in rubric, we don't enter this function at all :( + $criteria = $this->get_controller()->get_definition()->rubric_criteria; + if (!is_array($elementvalue) || sizeof($elementvalue) < sizeof($criteria)) { + return false; + } + foreach ($criteria as $id => $criterion) { + if (!array_key_exists($id, $elementvalue) || !array_key_exists($elementvalue[$id], $criterion['levels'])) { + return false; + } + } + return true; + } + + /** + * Retrieves from DB and returns the data how this rubric was filled + * + * @return array + */ + public function get_rubric_filling() { + // TODO cache + global $DB; + $rs = $DB->get_records('gradingform_rubric_fillings', array('forminstanceid' => $this->get_id())); + $grading = array(); + foreach ($rs as $record) { + if ($record->levelid) { + $grading[$record->criterionid] = $record->levelid; + } + // TODO: remarks + } + return $grading; + } + + /** + * Updates the instance with the data received from grading form. This function may be + * called via AJAX when grading is not yet completed, so it does not change the + * status of the instance. + */ + public function update($data) { + global $DB; + $currentgrade = $this->get_rubric_filling(); + parent::update($data); // TODO ? +timemodified + foreach ($data as $criterionid => $levelid) { + $params = array('forminstanceid' => $this->get_id(), 'criterionid' => $criterionid); + if (!array_key_exists($criterionid, $currentgrade)) { + $DB->insert_record('gradingform_rubric_fillings', $params + array('levelid' => $levelid)); + } else if ($currentgrade[$criterionid] != $levelid) { + $DB->set_field('gradingform_rubric_fillings', 'levelid', $levelid, $params); + } + } + foreach ($currentgrade as $criterionid => $levelid) { + if (!array_key_exists($criterionid, $data)) { + $params = array('forminstanceid' => $this->get_id(), 'criterionid' => $criterionid); + $DB->delete_records('gradingform_rubric_fillings', $params); + } + } + // TODO: remarks + } + + /** + * Calculates the grade to be pushed to the gradebook + * @return int the grade on 0-100 scale + */ + public function get_grade() { + global $DB, $USER; + $grade = $this->get_rubric_filling(); + + $minscore = 0; + $maxscore = 0; + foreach ($this->get_controller()->get_definition()->rubric_criteria as $id => $criterion) { + $keys = array_keys($criterion['levels']); + // TODO array_reverse($keys) if levels are sorted DESC + $minscore += $criterion['levels'][$keys[0]]['score']; + $maxscore += $criterion['levels'][$keys[sizeof($keys)-1]]['score']; + } + + if ($maxscore == 0) { + return -1; + } + + $curscore = 0; + foreach ($grade as $id => $levelid) { + $curscore += $this->get_controller()->get_definition()->rubric_criteria[$id]['levels'][$levelid]['score']; + } + return $curscore/$maxscore*100; // TODO mapping + } + + /** + * Returns the error message displayed in case of validation failed + * + * @return string + */ + public function default_validation_error_message() { + return 'The rubric is incomplete'; //TODO string + } + + /** + * Returns html for form element of type 'grading'. + * + * @param moodle_page $page + * @param MoodleQuickForm_grading $formelement + * @return string + */ + public function render_grading_element($page, $gradingformelement) { + global $USER; + if (!$gradingformelement->_flagFrozen) { + $module = array('name'=>'gradingform_rubric', 'fullpath'=>'/grade/grading/form/rubric/js/rubric.js'); + $page->requires->js_init_call('M.gradingform_rubric.init', array(array('name' => $gradingformelement->getName())), true, $module); + $mode = gradingform_rubric_controller::DISPLAY_EVAL; + } else { + if ($gradingformelement->_persistantFreeze) { + $mode = gradingform_rubric_controller::DISPLAY_EVAL_FROZEN; + } else { + $mode = gradingform_rubric_controller::DISPLAY_REVIEW; + } + } + $criteria = $this->get_controller()->get_definition()->rubric_criteria; + $value = $gradingformelement->getValue(); + if ($value === null) { + $value = $this->get_rubric_filling(); + } + return $this->get_controller()->get_renderer($page)->display_rubric($criteria, $mode, $gradingformelement->getName(), $value); + } +} \ No newline at end of file diff --git a/grade/grading/form/rubric/renderer.php b/grade/grading/form/rubric/renderer.php index 71b7e22c123..547e8091cd4 100644 --- a/grade/grading/form/rubric/renderer.php +++ b/grade/grading/form/rubric/renderer.php @@ -213,4 +213,35 @@ class gradingform_rubric_renderer { } return $class; } + + /** + * Displays for the student the list of instances or default content if no instances found + * + * @param array $instances array of objects of type gradingform_rubric_instance + * @param string $defaultcontent default string that would be displayed without advanced grading + * @return string + */ + public function display_instances($instances, $defaultcontent) { + if (sizeof($instances)) { + $rv = html_writer::start_tag('div', array('class' => 'advancedgrade')); + $idx = 0; + foreach ($instances as $instance) { + $rv .= $this->display_instance($instance, $idx++); + } + $rv .= html_writer::end_tag('div'); + } + return $rv. $defaultcontent; + } + + /** + * Displays one grading instance + * + * @param gradingform_rubric_instance $instance + * @param int idx unique number of instance on page + */ + public function display_instance(gradingform_rubric_instance $instance, $idx) { + $criteria = $instance->get_controller()->get_definition()->rubric_criteria; + $values = $instance->get_rubric_filling(); + return $this->display_rubric($criteria, gradingform_rubric_controller::DISPLAY_REVIEW, 'rubric'.$idx, $values); + } } diff --git a/grade/grading/form/rubric/styles.css b/grade/grading/form/rubric/styles.css index eed125d56bd..0168a207062 100644 --- a/grade/grading/form/rubric/styles.css +++ b/grade/grading/form/rubric/styles.css @@ -71,7 +71,7 @@ .form_rubric.editor .criterion.last .controls .movedown input {display:none;} /* evaluation */ -.form_rubric.evaluate .criterion .levels .level.checked {background:#d0ffd0;} +.form_rubric .criterion .levels .level.checked {background:#d0ffd0;} .form_rubric.evaluate .criterion .levels .level:hover {background:#30ff30;} /* replace buttons with images */ diff --git a/lib/form/grading.php b/lib/form/grading.php index ef11df553b1..b64ed1f7092 100644 --- a/lib/form/grading.php +++ b/lib/form/grading.php @@ -31,7 +31,11 @@ if (class_exists('HTML_QuickForm')) { } /** - * HTML class for a grading element + * HTML class for a grading element. This is a wrapper for advanced grading plugins. + * When adding the 'grading' element to the form, developer must pass an object of + * class gradingform_instance as $attributes['gradinginstance']. Otherwise an exception will be + * thrown. + * This object is responsible for implementing functions to render element html and validate it * * @author Marina Glancy * @access public @@ -44,6 +48,10 @@ class MoodleQuickForm_grading extends HTML_QuickForm_input{ */ var $_helpbutton=''; + /** + * Stores attributes passed to the element + * @var array + */ private $gradingattributes; function MoodleQuickForm_grading($elementName=null, $elementLabel=null, $attributes=null) { @@ -51,16 +59,27 @@ class MoodleQuickForm_grading extends HTML_QuickForm_input{ $this->gradingattributes = $attributes; } + /** + * Helper function to retrieve gradingform_instance passed in element attributes + * + * @return gradingform_instance + */ + function get_gradinginstance() { + if (is_array($this->gradingattributes) && array_key_exists('gradinginstance', $this->gradingattributes)) { + return $this->gradingattributes['gradinginstance']; + } else { + return null; + } + } + + /** + * Returns the input field in HTML + * + * @return string + */ function toHtml(){ - return $this->get_controller()->to_html($this); - } - - function get_grading_attribute($name) { - return $this->gradingattributes[$name]; - } - - function get_controller() { - return $this->get_grading_attribute('controller'); + global $PAGE; + return $this->get_gradinginstance()->render_grading_element($PAGE, $this); } /** @@ -97,22 +116,23 @@ class MoodleQuickForm_grading extends HTML_QuickForm_input{ function onQuickFormEvent($event, $arg, &$caller) { if ($event == 'createElement') { $attributes = $arg[2]; - if (!is_array($attributes) || !array_key_exists('controller', $attributes) || !($attributes['controller'] instanceof gradingform_controller)) { + if (!is_array($attributes) || !array_key_exists('gradinginstance', $attributes) || !($attributes['gradinginstance'] instanceof gradingform_instance)) { throw new moodle_exception('exc_gradingformelement', 'grading'); } } $name = $this->getName(); if ($name && $caller->elementExists($name)) { - $caller->addRule($name, $this->get_controller()->default_validation_error_message(), 'gradingvalidated', $this->gradingattributes); + $caller->addRule($name, $this->get_gradinginstance()->default_validation_error_message(), 'gradingvalidated', $this->gradingattributes); } return parent::onQuickFormEvent($event, $arg, $caller); } /** - * Function registered as rule for this element and is called when this element is being validated + * Function registered as rule for this element and is called when this element is being validated. + * This is a wrapper to pass the validation to the method gradingform_instance::validate_grading_element */ static function _validate($elementValue, $attributes = null) { - return $attributes['controller']->validate_grading_element($elementValue, $attributes['submissionid']); + return $attributes['gradinginstance']->validate_grading_element($elementValue); } } diff --git a/mod/assignment/lib.php b/mod/assignment/lib.php index d4096b14505..908ea30776f 100644 --- a/mod/assignment/lib.php +++ b/mod/assignment/lib.php @@ -264,8 +264,9 @@ class assignment_base { * @param object $submission The submission object or NULL in which case it will be loaded */ function view_feedback($submission=NULL) { - global $USER, $CFG, $DB, $OUTPUT; + global $USER, $CFG, $DB, $OUTPUT, $PAGE; require_once($CFG->libdir.'/gradelib.php'); + require_once("$CFG->dirroot/grade/grading/lib.php"); if (!is_enrolled($this->context, $USER, 'mod/assignment:view')) { // can not submit assignments -> no feedback @@ -329,9 +330,12 @@ class assignment_base { echo ''; echo ' '; echo ''; - echo '
'; - echo get_string("grade").': '.$grade->str_long_grade; - echo '
'; + $grade_str = '
'. get_string("grade").': '.$grade->str_long_grade. '
'; + if (!empty($submission) && $controller = get_grading_manager($this->context, 'mod_assignment', 'submission')->get_active_controller()) { + echo $controller->render_grade($PAGE, $submission->id, $grade_str); + } else { + echo $grade_str; + } echo '
'; echo '
'; @@ -608,7 +612,6 @@ class assignment_base { //make user global so we can use the id global $USER, $OUTPUT, $DB, $PAGE; - $mailinfo = optional_param('mailinfo', null, PARAM_BOOL); if (optional_param('next', null, PARAM_BOOL)) { @@ -628,12 +631,15 @@ class assignment_base { set_user_preference('assignment_mailinfo', $mailinfo); } + if (!($this->validate_and_preprocess_feedback())) { + // form was submitted ('Save' or 'Save and next' was pressed, but validation failed) + $this->display_submission(); + return; + } + switch ($mode) { case 'grade': // We are in a main window grading - if (!$this->validate_and_preprocess_feedback()) { - // validation failed - $this->display_submission(); - } else if ($submission = $this->process_feedback()) { + if ($submission = $this->process_feedback()) { $this->display_submissions(get_string('changessaved')); } else { $this->display_submissions(); @@ -747,11 +753,7 @@ class assignment_base { case 'saveandnext': ///We are in pop up. save the current one and go to the next one. //first we save the current changes - if (!$this->validate_and_preprocess_feedback()) { - // validation failed - $this->display_submission(); - break; - } else if ($submission = $this->process_feedback()) { + if ($submission = $this->process_feedback()) { //print_heading(get_string('changessaved')); //$extra_javascript = $this->update_main_listing($submission); } @@ -925,7 +927,7 @@ class assignment_base { * @param string $extra_javascript */ function display_submission($offset=-1,$userid =-1, $display=true) { - global $CFG, $DB, $PAGE, $OUTPUT; + global $CFG, $DB, $PAGE, $OUTPUT, $USER; require_once($CFG->libdir.'/gradelib.php'); require_once($CFG->libdir.'/tablelib.php'); require_once("$CFG->dirroot/repository/lib.php"); @@ -1046,41 +1048,12 @@ class assignment_base { } elseif ($assignment->assignmenttype == 'uploadsingle') { $mformdata->fileui_options = array('subdirs'=>0, 'maxbytes'=>$CFG->userquota, 'maxfiles'=>1, 'accepted_types'=>'*', 'return_types'=>FILE_INTERNAL); } - /*$gradingman = get_grading_manager($this->context, 'mod_assignment', 'submission'); - if ($gradingmethod = $gradingman->get_active_method()) { - $controller = $gradingman->get_controller($gradingmethod); - if ($controller->is_form_available()) { - if (empty($submission->id)) { - $mformdata->advancedgradingenabled = true; - $mformdata->advancedgradingwidget = get_string('noitemid', 'core_grading'); - } else { - $gradingrenderer = $controller->prepare_renderer($PAGE); - if ($this->assignment->grade < 0) { - $options = array( - 'displayas' => 'scale', - 'scaleid' => -$this->assignment->grade); - } else { - $options = array( - 'displayas' => 'grade', - 'maxgrade' => $this->assignment->grade, - 'decimals' => 0); - } - $gradingwidget = $controller->make_grading_widget($USER->id, $submission->id, $options); - if ($gradingwidget instanceof renderable) { - $mformdata->advancedgradingenabled = true; - $mformdata->advancedgradingwidget = $gradingrenderer->render($gradingwidget); - } - } - } else { - notice(get_string('formnotavailable', 'core_grading'), new moodle_url('/course/view.php', array('id' => $assignment->course))); - } - }*/ if ($controller = get_grading_manager($this->context, 'mod_assignment', 'submission')->get_active_controller()) { if (!isset($submission->id)) { - // TODO this is a patch if submission id does not exist yet + // we create a submission if it does not exist yet because we need submission->id for grading $mformdata->submission = $this->get_submission($user->id, true); } - $mformdata->advancedgradingcontroller = $controller; + $mformdata->advancedgradinginstance = $controller->create_instance($USER->id, $mformdata->submission->id); } $submitform = new mod_assignment_grading_form( null, $mformdata ); @@ -1591,28 +1564,35 @@ class assignment_base { } /** - * Validates the submitted form and returns false if validation did not pass. + * If the form was cancelled ('Cancel' or 'Next' was pressed), call cancel method + * from advanced grading (if applicable) and returns true + * If the form was submitted, validates it and returns false if validation did not pass. * If validation passes, preprocess advanced grading (if applicable) and returns true. */ function validate_and_preprocess_feedback() { global $USER; - if (!$feedback = data_submitted()) { + if (!($feedback = data_submitted()) || !isset($feedback->userid) || !isset($feedback->offset)) { return true; // No incoming data, nothing to validate } $userid = required_param('userid', PARAM_INT); $offset = required_param('offset', PARAM_INT); $submissiondata = $this->display_submission($offset, $userid, false); $mform = $submissiondata->mform; - if ($mform->is_submitted()) { + $gradinginstance = $mform->use_advanced_grading(); + if (optional_param('cancel', false, PARAM_BOOL) || optional_param('next', false, PARAM_BOOL)) { + // form was cancelled + if ($gradinginstance) { + $gradinginstance->cancel(); + } + } else if ($mform->is_submitted()) { + // form was submitted (= a submit button other than 'cancel' or 'next' has been clicked) if (!$mform->is_validated()) { return false; } // preprocess advanced grading here - if ($controller = $mform->use_advanced_grading()) { + if ($gradinginstance) { $data = $mform->get_data(); - // TODO find better way to find submission id - $submission = $this->get_submission($userid); - $_POST['xgrade'] = $controller->save_and_get_grade($USER->id /* TODO */, $submission->id, $data->advancedgrading); + $_POST['xgrade'] = $gradinginstance->submit_and_get_grade($data->advancedgrading); } } return true; @@ -2300,8 +2280,8 @@ class mod_assignment_grading_form extends moodleform { global $OUTPUT; $mform =& $this->_form; - if (isset($this->_customdata->advancedgradingcontroller)) { - $this->use_advanced_grading($this->_customdata->advancedgradingcontroller); + if (isset($this->_customdata->advancedgradinginstance)) { + $this->use_advanced_grading($this->_customdata->advancedgradinginstance); } $formattr = $mform->getAttributes(); @@ -2349,17 +2329,17 @@ class mod_assignment_grading_form extends moodleform { } - private $_advancegradingcontroller; + private $advancegradinginstance; /** - * Gets or sets the controller for advanced grading + * Gets or sets the instance for advanced grading * - * @param $controller + * @param gradingform_instance $gradinginstance */ - public function use_advanced_grading($controller = false) { - if ($controller !== false) { - $this->_advancegradingcontroller = $controller; + public function use_advanced_grading($gradinginstance = false) { + if ($gradinginstance !== false) { + $this->advancegradinginstance = $gradinginstance; } - return $this->_advancegradingcontroller; + return $this->advancegradinginstance; } function add_grades_section() { @@ -2372,10 +2352,8 @@ class mod_assignment_grading_form extends moodleform { $mform->addElement('header', 'Grades', get_string('grades', 'grades')); - if ($controller = $this->use_advanced_grading()) { - // TODO what if submission id does not exist yet! - $mform->addElement('grading', 'advancedgrading', get_string('grade').':', - array('controller' => $controller, 'submissionid' => $this->_customdata->submission->id)); + if ($gradinginstance = $this->use_advanced_grading()) { + $mform->addElement('grading', 'advancedgrading', get_string('grade').':', array('gradinginstance' => $gradinginstance)); } else { // use simple direct grading $grademenu = make_grades_menu($this->_customdata->assignment->grade);