MDL-29483: advanced grading methods support on assignment module, grading form element, rubrics

- gradingform API now has not only controller class but also instance class with functions and data related to the particular grading instance
- assignment grading form and grading form element deal with elements of gradingform_instance class instead of gradingform_controller
- implemented the class gradingform_rubric_instance
- added more phpdocs
This commit is contained in:
Marina Glancy 2011-10-20 12:15:51 +08:00
parent 671ec8f50e
commit 36937f0264
6 changed files with 580 additions and 326 deletions

View File

@ -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 '';
}
}

View File

@ -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<br />";
//$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);
}
}

View File

@ -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);
}
}

View File

@ -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 */

View File

@ -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);
}
}

View File

@ -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 '<tr>';
echo '<td class="left side">&nbsp;</td>';
echo '<td class="content">';
echo '<div class="grade">';
echo get_string("grade").': '.$grade->str_long_grade;
echo '</div>';
$grade_str = '<div class="grade">'. get_string("grade").': '.$grade->str_long_grade. '</div>';
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 '<div class="clearer"></div>';
echo '<div class="comment">';
@ -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 <type> $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);