MDL-19932 Rubric grading strategy implemented

The only weak point here is that we store a raw grade into
workshop_grades and not a direct id of the selected level. Therefore,
when re-assessing, we need to actually guess what level the assessor
chose previously. This is not problem if there are not two levels with
the same grade. Such case is not common when using Rubric. In the
future, this may get refactored so Rubric would use its own storage of
filled assessment forms.
This commit is contained in:
David Mudrak
2010-01-04 18:16:02 +00:00
parent d5506aac98
commit d524173efe
7 changed files with 622 additions and 416 deletions
+102 -16
View File
@@ -16,7 +16,10 @@
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* This file defines an mform to assess a submission by rubric grading strategy
* This file defines mforms to assess a submission by rubric grading strategy
*
* Rubric can be displayed in two possible layouts - list or grid. This file defines
* therefore defines two classes, respectively.
*
* @package mod-workshop
* @copyright 2009 David Mudrak <[email protected]>
@@ -28,11 +31,27 @@ defined('MOODLE_INTERNAL') || die();
require_once(dirname(dirname(__FILE__)).'/assessment_form.php'); // parent class definition
/**
* Class representing a form for assessing submissions by rubric grading strategy
*
* @uses moodleform
* Base class representing a form for assessing submissions by rubric grading strategy
*/
class workshop_rubric_assessment_form extends workshop_assessment_form {
abstract class workshop_rubric_assessment_form extends workshop_assessment_form {
public function validation($data, $files) {
$errors = parent::validation($data, $files);
for ($i = 0; isset($data['dimensionid__idx_'.$i]); $i++) {
if (empty($data['chosenlevelid__idx_'.$i])) {
$errors['chosenlevelid__idx_'.$i] = get_string('mustchooseone', 'workshopform_rubric'); // used in grid
$errors['levelgrp__idx_'.$i] = get_string('mustchooseone', 'workshopform_rubric'); // used in list
}
}
return $errors;
}
}
/**
* Class representing a form for assessing submissions by rubric grading strategy - list layout
*/
class workshop_rubric_list_assessment_form extends workshop_rubric_assessment_form {
/**
* Define the elements to be displayed at the form
@@ -42,12 +61,11 @@ class workshop_rubric_assessment_form extends workshop_assessment_form {
* @return void
*/
protected function definition_inner(&$mform) {
$workshop = $this->_customdata['workshop'];
$fields = $this->_customdata['fields'];
$current = $this->_customdata['current'];
$nodims = $this->_customdata['nodims']; // number of assessment dimensions
$mform->addElement('hidden', 'nodims', $nodims);
for ($i = 0; $i < $nodims; $i++) {
// dimension header
$dimtitle = get_string('dimensionnumber', 'workshopform_rubric', $i+1);
@@ -65,16 +83,84 @@ class workshop_rubric_assessment_form extends workshop_assessment_form {
$desc .= "\n</div>";
$mform->addElement('html', $desc);
// grade for this aspect
$label = get_string('dimensiongrade', 'workshopform_rubric');
$options = make_grades_menu($fields->{'grade__idx_' . $i});
$mform->addElement('select', 'grade__idx_' . $i, $label, $options);
// comment
$label = get_string('dimensioncomment', 'workshopform_rubric');
//$mform->addElement('editor', 'peercomment__idx_' . $i, $label, null, array('maxfiles' => 0));
$mform->addElement('textarea', 'peercomment__idx_' . $i, $label, array('cols' => 60, 'rows' => 5));
$numoflevels = $fields->{'numoflevels__idx_'.$i};
$levelgrp = array();
for ($j = 0; $j < $numoflevels; $j++) {
$levelid = $fields->{'levelid__idx_'.$i.'__idy_'.$j};
$definition = $fields->{'definition__idx_'.$i.'__idy_'.$j};
$definitionformat = $fields->{'definition__idx_'.$i.'__idy_'.$j.'format'};
$levelgrp[] = $mform->createElement('radio', 'chosenlevelid__idx_'.$i, '',
format_text($definition, $definitionformat, null, $workshop->course->id), $levelid);
}
$mform->addGroup($levelgrp, 'levelgrp__idx_'.$i, '', "<br />\n", false);
}
$this->set_data($current);
}
}
/**
* Class representing a form for assessing submissions by rubric grading strategy - grid layout
*/
class workshop_rubric_grid_assessment_form extends workshop_rubric_assessment_form {
/**
* Define the elements to be displayed at the form
*
* Called by the parent::definition()
*
* @return void
*/
protected function definition_inner(&$mform) {
$workshop = $this->_customdata['workshop'];
$fields = $this->_customdata['fields'];
$current = $this->_customdata['current'];
$nodims = $this->_customdata['nodims']; // number of assessment dimensions
// get the number of required grid columns
$levelcounts = array();
for ($i = 0; $i < $nodims; $i++) {
if ($fields->{'numoflevels__idx_'.$i} > 0) {
$levelcounts[] = $fields->{'numoflevels__idx_'.$i};
}
}
$numofcolumns = array_reduce($levelcounts, 'workshop::lcm', 1);
$mform->addElement('header', 'rubric-grid-wrapper', get_string('layoutgrid', 'workshopform_rubric'));
$mform->addElement('html', '<table class="rubric-grid">' . "\n");
$mform->addElement('html', '<th class="header">' . get_string('criteria', 'workshopform_rubric') . '</th>');
$mform->addElement('html', '<th class="header" colspan="'.$numofcolumns.'">'.get_string('levels', 'workshopform_rubric').'</th>');
for ($i = 0; $i < $nodims; $i++) {
$mform->addElement('html', '<tr class="r'. $i % 2 .'"><td class="criterion">' . "\n");
// dimension id
$mform->addElement('hidden', 'dimensionid__idx_'.$i, $fields->{'dimensionid__idx_'.$i});
// given grade id
$mform->addElement('hidden', 'gradeid__idx_'.$i); // value set by set_data() later
// dimension description
$desc = format_text($fields->{'description__idx_'.$i}, $fields->{'description__idx_'.$i.'format'});
$desc .= "</td>\n";
$mform->addElement('html', $desc);
$numoflevels = $fields->{'numoflevels__idx_'.$i};
for ($j = 0; $j < $numoflevels; $j++) {
$colspan = $numofcolumns / $numoflevels;
$mform->addElement('html', '<td class="level c' . $j % 2 . '" colspan="' . $colspan . '">' . "\n");
$levelid = $fields->{'levelid__idx_'.$i.'__idy_'.$j};
$definition = $fields->{'definition__idx_'.$i.'__idy_'.$j};
$definitionformat = $fields->{'definition__idx_'.$i.'__idy_'.$j.'format'};
$mform->addElement('radio', 'chosenlevelid__idx_'.$i, '',
format_text($definition, $definitionformat, null, $workshop->course->id), $levelid);
$mform->addElement('html', '</td>' . "\n");
}
$mform->addElement('html', '</tr>' . "\n");
}
$mform->addElement('html', '</table>' . "\n");
$this->set_data($current);
}
}
+1 -2
View File
@@ -47,7 +47,6 @@ class workshop_edit_rubric_strategy_form extends workshop_edit_strategy_form {
protected function definition_inner(&$mform) {
$norepeats = $this->_customdata['norepeats']; // number of dimensions to display
$addlevels = $this->_customdata['addlevels']; // additional levels required
$descriptionopts = $this->_customdata['descriptionopts']; // wysiwyg fields options
$current = $this->_customdata['current']; // current data to be set
@@ -70,7 +69,7 @@ class workshop_edit_rubric_strategy_form extends workshop_edit_strategy_form {
} else {
$numoflevels = self::MINLEVELS;
}
$prevlevel = 0;
$prevlevel = -1;
for ($j = 0; $j < $numoflevels; $j++) {
$mform->addElement('hidden', 'levelid__idx_' . $i . '__idy_' . $j);
$levelgrp = array();
@@ -34,3 +34,6 @@ $string['layoutlist'] = 'List';
$string['layout'] = 'Rubric layout';
$string['levelgroup'] = 'Level grade and definition';
$string['pluginname'] = 'Rubric';
$string['criteria'] = 'Criteria';
$string['levels'] = 'Levels';
$string['mustchooseone'] = 'You have to select one of these items';
+364 -391
View File
@@ -65,389 +65,394 @@ class workshop_rubric_strategy implements workshop_strategy {
$this->dimensions = $this->load_fields();
$this->config = $this->load_config();
$this->descriptionopts = array('trusttext' => true, 'subdirs' => false, 'maxfiles' => -1);
//one day the definitions may become proper wysiwyg fields - not used yet
$this->definitionopts = array('trusttext' => true, 'subdirs' => false, 'maxfiles' => -1);
}
/**
* Factory method returning an instance of an assessment form editor class
*
* @param $actionurl URL of form handler, defaults to auto detect the current url
*/
public function get_edit_strategy_form($actionurl=null) {
global $CFG; // needed because the included files use it
global $DB;
/**
* Factory method returning an instance of an assessment form editor class
*
* @param $actionurl URL of form handler, defaults to auto detect the current url
*/
public function get_edit_strategy_form($actionurl=null) {
global $CFG; // needed because the included files use it
require_once(dirname(__FILE__) . '/edit_form.php');
require_once(dirname(__FILE__) . '/edit_form.php');
$fields = $this->prepare_form_fields($this->dimensions);
$fields->config_layout = $DB->get_field('workshopform_rubric_config', 'layout', array('workshopid' => $this->workshop->id));
$fields = $this->prepare_form_fields($this->dimensions);
$fields->config_layout = $this->config->layout;
$nodimensions = count($this->dimensions);
$norepeatsdefault = max($nodimensions + self::ADDDIMS, self::MINDIMS);
$norepeats = optional_param('norepeats', $norepeatsdefault, PARAM_INT); // number of dimensions
$adddims = optional_param('adddims', '', PARAM_ALPHA); // shall we add more dimensions?
$addlevels = optional_param('addlevels', '', PARAM_ALPHA); // shall we add more dimensions?
if ($adddims) {
$norepeats += self::ADDDIMS;
}
$addlevels = (bool)$addlevels; // string to boolean
// prepare the embeded files
for ($i = 0; $i < $nodimensions; $i++) {
// prepare all editor elements
$fields = file_prepare_standard_editor($fields, 'description__idx_'.$i, $this->descriptionopts,
$this->workshop->context, 'workshopform_rubric_description', $fields->{'dimensionid__idx_'.$i});
}
$customdata = array();
$customdata['workshop'] = $this->workshop;
$customdata['strategy'] = $this;
$customdata['norepeats'] = $norepeats;
$customdata['addlevels'] = $addlevels;
$customdata['descriptionopts'] = $this->descriptionopts;
$customdata['current'] = $fields;
$attributes = array('class' => 'editstrategyform');
return new workshop_edit_rubric_strategy_form($actionurl, $customdata, 'post', '', $attributes);
}
/**
* Save the assessment dimensions into database
*
* Saves data into the main strategy form table. If the record->id is null or zero,
* new record is created. If the record->id is not empty, the existing record is updated. Records with
* empty 'description' field are removed from database.
* The passed data object are the raw data returned by the get_data().
*
* @uses $DB
* @param stdClass $data Raw data returned by the dimension editor form
* @return void
*/
public function save_edit_strategy_form(stdClass $data) {
global $DB;
$norepeats = $data->norepeats;
$layout = $data->config_layout;
$data = $this->prepare_database_fields($data);
$deletedims = array(); // dimension ids to be deleted
$deletelevs = array(); // level ids to be deleted
if ($DB->record_exists('workshopform_rubric_config', array('workshopid' => $this->workshop->id))) {
$DB->set_field('workshopform_rubric_config', 'layout', $layout, array('workshopid' => $this->workshop->id));
} else {
$record = new stdClass();
$record->workshopid = $this->workshop->id;
$record->layout = $layout;
$DB->insert_record('workshopform_rubric_config', $record, false);
}
foreach ($data as $record) {
if (0 == strlen(trim($record->description_editor['text']))) {
if (!empty($record->id)) {
// existing record with empty description - to be deleted
$deletedims[] = $record->id;
foreach ($record->levels as $level) {
if (!empty($level->id)) {
$deletelevs[] = $level->id;
}
}
}
continue;
$nodimensions = count($this->dimensions);
$norepeatsdefault = max($nodimensions + self::ADDDIMS, self::MINDIMS);
$norepeats = optional_param('norepeats', $norepeatsdefault, PARAM_INT); // number of dimensions
$adddims = optional_param('adddims', '', PARAM_ALPHA); // shall we add more dimensions?
if ($adddims) {
$norepeats += self::ADDDIMS;
}
if (empty($record->id)) {
// new field
$record->id = $DB->insert_record('workshopform_rubric', $record);
// prepare the embeded files
for ($i = 0; $i < $nodimensions; $i++) {
// prepare all editor elements
$fields = file_prepare_standard_editor($fields, 'description__idx_'.$i, $this->descriptionopts,
$this->workshop->context, 'workshopform_rubric_description', $fields->{'dimensionid__idx_'.$i});
}
$customdata = array();
$customdata['workshop'] = $this->workshop;
$customdata['strategy'] = $this;
$customdata['norepeats'] = $norepeats;
$customdata['descriptionopts'] = $this->descriptionopts;
$customdata['current'] = $fields;
$attributes = array('class' => 'editstrategyform');
return new workshop_edit_rubric_strategy_form($actionurl, $customdata, 'post', '', $attributes);
}
/**
* Save the assessment dimensions into database
*
* Saves data into the main strategy form table. If the record->id is null or zero,
* new record is created. If the record->id is not empty, the existing record is updated. Records with
* empty 'description' field are removed from database.
* The passed data object are the raw data returned by the get_data().
*
* @uses $DB
* @param stdClass $data Raw data returned by the dimension editor form
* @return void
*/
public function save_edit_strategy_form(stdClass $data) {
global $DB;
$norepeats = $data->norepeats;
$layout = $data->config_layout;
$data = $this->prepare_database_fields($data);
$deletedims = array(); // dimension ids to be deleted
$deletelevs = array(); // level ids to be deleted
if ($DB->record_exists('workshopform_rubric_config', array('workshopid' => $this->workshop->id))) {
$DB->set_field('workshopform_rubric_config', 'layout', $layout, array('workshopid' => $this->workshop->id));
} else {
// exiting field
$DB->update_record('workshopform_rubric', $record);
$record = new stdClass();
$record->workshopid = $this->workshop->id;
$record->layout = $layout;
$DB->insert_record('workshopform_rubric_config', $record, false);
}
// re-save with correct path to embeded media files
$record = file_postupdate_standard_editor($record, 'description', $this->descriptionopts,
$this->workshop->context, 'workshopform_rubric_description', $record->id);
$DB->update_record('workshopform_rubric', $record);
// create/update the criterion levels
foreach ($record->levels as $level) {
$level->dimensionid = $record->id;
if (0 == strlen(trim($level->definition))) {
if (!empty($level->id)) {
$deletelevs[] = $level->id;
foreach ($data as $record) {
if (0 == strlen(trim($record->description_editor['text']))) {
if (!empty($record->id)) {
// existing record with empty description - to be deleted
$deletedims[] = $record->id;
foreach ($record->levels as $level) {
if (!empty($level->id)) {
$deletelevs[] = $level->id;
}
}
}
continue;
}
if (empty($level->id)) {
if (empty($record->id)) {
// new field
$level->id = $DB->insert_record('workshopform_rubric_levels', $level);
$record->id = $DB->insert_record('workshopform_rubric', $record);
} else {
// exiting field
$DB->update_record('workshopform_rubric_levels', $level);
$DB->update_record('workshopform_rubric', $record);
}
// re-save with correct path to embeded media files
$record = file_postupdate_standard_editor($record, 'description', $this->descriptionopts,
$this->workshop->context, 'workshopform_rubric_description', $record->id);
$DB->update_record('workshopform_rubric', $record);
// create/update the criterion levels
foreach ($record->levels as $level) {
$level->dimensionid = $record->id;
if (0 == strlen(trim($level->definition))) {
if (!empty($level->id)) {
$deletelevs[] = $level->id;
}
continue;
}
if (empty($level->id)) {
// new field
$level->id = $DB->insert_record('workshopform_rubric_levels', $level);
} else {
// exiting field
$DB->update_record('workshopform_rubric_levels', $level);
}
}
}
}
$DB->delete_records_list('workshopform_rubric_levels', 'id', $deletelevs);
$this->delete_dimensions($deletedims);
}
/**
* Factory method returning an instance of an assessment form
*
* @param moodle_url $actionurl URL of form handler, defaults to auto detect the current url
* @param string $mode Mode to open the form in: preview/assessment
*/
public function get_assessment_form(moodle_url $actionurl=null, $mode='preview', stdClass $assessment=null) {
global $CFG; // needed because the included files use it
global $DB;
require_once(dirname(__FILE__) . '/assessment_form.php');
$fields = $this->prepare_form_fields($this->dimensions);
$nodimensions = count($this->dimensions);
// rewrite URLs to the embeded files
for ($i = 0; $i < $nodimensions; $i++) {
$fields->{'description__idx_'.$i} = file_rewrite_pluginfile_urls($fields->{'description__idx_'.$i},
'pluginfile.php', $this->workshop->context->id, 'workshopform_rubric_description', $fields->{'dimensionid__idx_'.$i});
$DB->delete_records_list('workshopform_rubric_levels', 'id', $deletelevs);
$this->delete_dimensions($deletedims);
}
if ('assessment' === $mode and !empty($assessment)) {
// load the previously saved assessment data
$grades = $this->get_current_assessment_data($assessment);
$current = new stdClass();
/**
* Factory method returning an instance of an assessment form
*
* @param moodle_url $actionurl URL of form handler, defaults to auto detect the current url
* @param string $mode Mode to open the form in: preview/assessment
*/
public function get_assessment_form(moodle_url $actionurl=null, $mode='preview', stdClass $assessment=null) {
global $CFG; // needed because the included files use it
global $DB;
require_once(dirname(__FILE__) . '/assessment_form.php');
$fields = $this->prepare_form_fields($this->dimensions);
$nodimensions = count($this->dimensions);
// rewrite URLs to the embeded files
for ($i = 0; $i < $nodimensions; $i++) {
$dimid = $fields->{'dimensionid__idx_'.$i};
if (isset($grades[$dimid])) {
$current->{'gradeid__idx_'.$i} = $grades[$dimid]->id;
$current->{'grade__idx_'.$i} = $grades[$dimid]->grade;
$current->{'peercomment__idx_'.$i} = $grades[$dimid]->peercomment;
$fields->{'description__idx_'.$i} = file_rewrite_pluginfile_urls($fields->{'description__idx_'.$i},
'pluginfile.php', $this->workshop->context->id, 'workshopform_rubric_description',
$fields->{'dimensionid__idx_'.$i});
}
if ('assessment' === $mode and !empty($assessment)) {
// load the previously saved assessment data
$grades = $this->get_current_assessment_data($assessment);
$current = new stdClass();
for ($i = 0; $i < $nodimensions; $i++) {
$dimid = $fields->{'dimensionid__idx_'.$i};
if (isset($grades[$dimid])) {
$givengrade = $grades[$dimid]->grade;
// find a level with this grade
$levelid = null;
foreach ($this->dimensions[$dimid]->levels as $level) {
if (grade_floats_equal($level->grade, $givengrade)) {
$levelid = $level->id;
break;
}
}
$current->{'gradeid__idx_'.$i} = $grades[$dimid]->id;
$current->{'chosenlevelid__idx_'.$i} = $levelid;
}
}
}
// set up the required custom data common for all strategies
$customdata['strategy'] = $this;
$customdata['workshop'] = $this->workshop;
$customdata['mode'] = $mode;
// set up strategy-specific custom data
$customdata['nodims'] = $nodimensions;
$customdata['fields'] = $fields;
$customdata['current'] = isset($current) ? $current : null;
$attributes = array('class' => 'assessmentform rubric ' . $this->config->layout);
$formclassname = 'workshop_rubric_' . $this->config->layout . '_assessment_form';
return new $formclassname($actionurl, $customdata, 'post', '', $attributes);
}
// set up the required custom data common for all strategies
$customdata['strategy'] = $this;
$customdata['workshop'] = $this->workshop;
$customdata['mode'] = $mode;
/**
* Saves the filled assessment
*
* This method processes data submitted using the form returned by {@link get_assessment_form()}
*
* @param stdClass $assessment Assessment being filled
* @param stdClass $data Raw data as returned by the assessment form
* @return float|null Raw grade (0.00000 to 100.00000) for submission as suggested by the peer
*/
public function save_assessment(stdClass $assessment, stdClass $data) {
global $DB;
// set up strategy-specific custom data
$customdata['nodims'] = $nodimensions;
$customdata['fields'] = $fields;
$customdata['current'] = isset($current) ? $current : null;
$attributes = array('class' => 'assessmentform rubric');
for ($i = 0; isset($data->{'dimensionid__idx_' . $i}); $i++) {
$grade = new stdClass();
$grade->id = $data->{'gradeid__idx_' . $i};
$grade->assessmentid = $assessment->id;
$grade->strategy = 'rubric';
$grade->dimensionid = $data->{'dimensionid__idx_' . $i};
$chosenlevel = $data->{'chosenlevelid__idx_'.$i};
$grade->grade = $this->dimensions[$grade->dimensionid]->levels[$chosenlevel]->grade;
return new workshop_rubric_assessment_form($actionurl, $customdata, 'post', '', $attributes);
}
/**
* Saves the filled assessment
*
* This method processes data submitted using the form returned by {@link get_assessment_form()}
*
* @param stdClass $assessment Assessment being filled
* @param stdClass $data Raw data as returned by the assessment form
* @return float|null Raw grade (0.00000 to 100.00000) for submission as suggested by the peer
*/
public function save_assessment(stdClass $assessment, stdClass $data) {
global $DB;
if (!isset($data->nodims)) {
throw coding_expection('You did not send me the number of assessment dimensions to process');
if (empty($grade->id)) {
// new grade
$grade->id = $DB->insert_record('workshop_grades', $grade);
} else {
// updated grade
$DB->update_record('workshop_grades', $grade);
}
}
return $this->update_peer_grade($assessment);
}
for ($i = 0; $i < $data->nodims; $i++) {
$grade = new stdClass();
$grade->id = $data->{'gradeid__idx_' . $i};
$grade->assessmentid = $assessment->id;
$grade->strategy = 'rubric';
$grade->dimensionid = $data->{'dimensionid__idx_' . $i};
$grade->grade = $data->{'grade__idx_' . $i};
$grade->peercomment = $data->{'peercomment__idx_' . $i};
$grade->peercommentformat = FORMAT_MOODLE;
if (empty($grade->id)) {
// new grade
$grade->id = $DB->insert_record('workshop_grades', $grade);
/**
* Has the assessment form been defined and is ready to be used by the reviewers?
*
* @return boolean
*/
public function form_ready() {
if (count($this->dimensions) > 0) {
return true;
}
return false;
}
/**
* Returns true if the given evaluation method is supported by this strategy
*
* To support an evaluation method, the strategy subplugin must usually implement some
* required public methods. In theory, this is what interfaces should be used for.
* Unfortunatelly, we can't extend "implements" declaration as the interface must
* be known to the PHP interpret. So we can't declare implementation of a non-installed
* evaluation subplugin.
*
* @param workshop_evaluation $evaluation the instance of grading evaluation class
* @return bool true if the evaluation method is supported, false otherwise
*/
public function supports_evaluation(workshop_evaluation $evaluation) {
if (is_a($evaluation, 'workshop_best_evaluation')) {
return true;
}
// all other evaluation methods are not supported yet
return false;
}
////////////////////////////////////////////////////////////////////////////////
// Methods required by the 'best' evaluation plugin //
////////////////////////////////////////////////////////////////////////////////
/**
* TODO
*
* @param resource $restrict
* @return TODO
*/
public function eval_best_get_assessments_recordset($restrict) {
global $DB;
$sql = 'SELECT s.id AS submissionid,
a.id AS assessmentid, a.weight AS assessmentweight, a.reviewerid, a.gradinggrade,
g.dimensionid, g.grade
FROM {workshop_submissions} s
JOIN {workshop_assessments} a ON (a.submissionid = s.id)
JOIN {workshop_grades} g ON (g.assessmentid = a.id AND g.strategy = :strategy)
WHERE s.example=0 AND s.workshopid=:workshopid'; // to be cont.
$params = array('workshopid' => $this->workshop->id, 'strategy' => $this->workshop->strategy);
if (is_null($restrict)) {
// update all users - no more conditions
} elseif (!empty($restrict)) {
list($usql, $uparams) = $DB->get_in_or_equal($restrict, SQL_PARAMS_NAMED);
$sql .= " AND a.reviewerid $usql";
$params = array_merge($params, $uparams);
} else {
// updated grade
$DB->update_record('workshop_grades', $grade);
throw new coding_exception('Empty value is not a valid parameter here');
}
}
return $this->update_peer_grade($assessment);
}
/**
* Has the assessment form been defined and is ready to be used by the reviewers?
*
* @return boolean
*/
public function form_ready() {
if (count($this->dimensions) > 0) {
return true;
}
return false;
}
$sql .= ' ORDER BY s.id'; // this is important for bulk processing
/**
* Returns true if the given evaluation method is supported by this strategy
*
* To support an evaluation method, the strategy subplugin must usually implement some
* required public methods. In theory, this is what interfaces should be used for.
* Unfortunatelly, we can't extend "implements" declaration as the interface must
* be known to the PHP interpret. So we can't declare implementation of a non-installed
* evaluation subplugin.
*
* @param workshop_evaluation $evaluation the instance of grading evaluation class
* @return bool true if the evaluation method is supported, false otherwise
*/
public function supports_evaluation(workshop_evaluation $evaluation) {
if (is_a($evaluation, 'workshop_best_evaluation')) {
return true;
}
// all other evaluation methods are not supported yet
return false;
}
////////////////////////////////////////////////////////////////////////////////
// Methods required by the 'best' evaluation plugin //
////////////////////////////////////////////////////////////////////////////////
/**
* TODO
*
* @param resource $restrict
* @return TODO
*/
public function eval_best_get_assessments_recordset($restrict) {
global $DB;
$sql = 'SELECT s.id AS submissionid,
a.id AS assessmentid, a.weight AS assessmentweight, a.reviewerid, a.gradinggrade,
g.dimensionid, g.grade
FROM {workshop_submissions} s
JOIN {workshop_assessments} a ON (a.submissionid = s.id)
JOIN {workshop_grades} g ON (g.assessmentid = a.id AND g.strategy = :strategy)
WHERE s.example=0 AND s.workshopid=:workshopid'; // to be cont.
$params = array('workshopid' => $this->workshop->id, 'strategy' => $this->workshop->strategy);
if (is_null($restrict)) {
// update all users - no more conditions
} elseif (!empty($restrict)) {
list($usql, $uparams) = $DB->get_in_or_equal($restrict, SQL_PARAMS_NAMED);
$sql .= " AND a.reviewerid $usql";
$params = array_merge($params, $uparams);
} else {
throw new coding_exception('Empty value is not a valid parameter here');
return $DB->get_recordset_sql($sql, $params);
}
$sql .= ' ORDER BY s.id'; // this is important for bulk processing
/**
* TODO: short description.
*
* @return array [dimid] => stdClass (->id ->max ->min ->weight)
*/
public function eval_best_dimensions_info() {
global $DB;
return $DB->get_recordset_sql($sql, $params);
}
/**
* TODO: short description.
*
* @return array [dimid] => stdClass (->id ->max ->min ->weight)
*/
public function eval_best_dimensions_info() {
global $DB;
$sql = 'SELECT d.id, d.grade, d.weight, s.scale
FROM {workshopform_rubric} d
LEFT JOIN {scale} s ON (d.grade < 0 AND -d.grade = s.id)
WHERE d.workshopid = :workshopid';
$params = array('workshopid' => $this->workshop->id);
$dimrecords = $DB->get_records_sql($sql, $params);
$diminfo = array();
foreach ($dimrecords as $dimid => $dimrecord) {
$diminfo[$dimid] = new stdClass();
$diminfo[$dimid]->id = $dimid;
$diminfo[$dimid]->weight = $dimrecord->weight;
if ($dimrecord->grade < 0) {
// the dimension uses a scale
$diminfo[$dimid]->min = 1;
$diminfo[$dimid]->max = count(explode(',', $dimrecord->scale));
} else {
// the dimension uses points
$diminfo[$dimid]->min = 0;
$diminfo[$dimid]->max = grade_floatval($dimrecord->grade);
$sql = 'SELECT d.id, d.grade, d.weight, s.scale
FROM {workshopform_rubric} d
LEFT JOIN {scale} s ON (d.grade < 0 AND -d.grade = s.id)
WHERE d.workshopid = :workshopid';
$params = array('workshopid' => $this->workshop->id);
$dimrecords = $DB->get_records_sql($sql, $params);
$diminfo = array();
foreach ($dimrecords as $dimid => $dimrecord) {
$diminfo[$dimid] = new stdClass();
$diminfo[$dimid]->id = $dimid;
$diminfo[$dimid]->weight = $dimrecord->weight;
if ($dimrecord->grade < 0) {
// the dimension uses a scale
$diminfo[$dimid]->min = 1;
$diminfo[$dimid]->max = count(explode(',', $dimrecord->scale));
} else {
// the dimension uses points
$diminfo[$dimid]->min = 0;
$diminfo[$dimid]->max = grade_floatval($dimrecord->grade);
}
}
return $diminfo;
}
return $diminfo;
}
////////////////////////////////////////////////////////////////////////////////
// Internal methods //
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
// Internal methods //
////////////////////////////////////////////////////////////////////////////////
/**
* Loads the fields of the assessment form currently used in this workshop
*
* @return array definition of assessment dimensions
*/
protected function load_fields() {
global $DB;
/**
* Loads the fields of the assessment form currently used in this workshop
*
* @return array definition of assessment dimensions
*/
protected function load_fields() {
global $DB;
$sql = 'SELECT r.id AS rid, l.id AS lid, *
FROM {workshopform_rubric} r
LEFT JOIN {workshopform_rubric_levels} l ON (l.dimensionid = r.id)
WHERE r.workshopid = :workshopid
ORDER BY r.sort, l.grade';
$params = array('workshopid' => $this->workshop->id);
$sql = 'SELECT l.id AS lid, r.id AS rid, *
FROM {workshopform_rubric} r
LEFT JOIN {workshopform_rubric_levels} l ON (l.dimensionid = r.id)
WHERE r.workshopid = :workshopid
ORDER BY r.sort, l.grade';
$params = array('workshopid' => $this->workshop->id);
$rs = $DB->get_recordset_sql($sql, $params);
$fields = array();
foreach ($rs as $record) {
if (!isset($fields[$record->rid])) {
$fields[$record->rid] = new stdClass();
$fields[$record->rid]->id = $record->rid;
$fields[$record->rid]->sort = $record->sort;
$fields[$record->rid]->description = $record->description;
$fields[$record->rid]->descriptionformat = $record->descriptionformat;
$fields[$record->rid]->levels = array();
$records = $DB->get_records_sql($sql, $params);
$fields = array();
foreach ($records as $record) {
if (!isset($fields[$record->rid])) {
$fields[$record->rid] = new stdClass();
$fields[$record->rid]->id = $record->rid;
$fields[$record->rid]->sort = $record->sort;
$fields[$record->rid]->description = $record->description;
$fields[$record->rid]->descriptionformat = $record->descriptionformat;
$fields[$record->rid]->levels = array();
}
if (!empty($record->lid)) {
$fields[$record->rid]->levels[$record->lid] = new stdClass();
$fields[$record->rid]->levels[$record->lid]->id = $record->lid;
$fields[$record->rid]->levels[$record->lid]->grade = $record->grade;
$fields[$record->rid]->levels[$record->lid]->definition = $record->definition;
$fields[$record->rid]->levels[$record->lid]->definitionformat = $record->definitionformat;
}
}
$fields[$record->rid]->levels[$record->lid] = new stdClass();
$fields[$record->rid]->levels[$record->lid]->id = $record->lid;
$fields[$record->rid]->levels[$record->lid]->grade = $record->grade;
$fields[$record->rid]->levels[$record->lid]->definition = $record->definition;
$fields[$record->rid]->levels[$record->lid]->definitionformat = $record->definitionformat;
return $fields;
}
$rs->close();
return $fields;
}
/**
* Get the configuration for the current rubric strategy
*
* @return object
*/
protected function load_config() {
global $DB;
/**
* Get the configuration for the current rubric strategy
*
* @return object
*/
protected function load_config() {
global $DB;
if (!$config = $DB->get_record('workshopform_rubric_config', array('workshopid' => $this->workshop->id), 'layout')) {
$config = (object)array('layout' => 'list');
if (!$config = $DB->get_record('workshopform_rubric_config', array('workshopid' => $this->workshop->id), 'layout')) {
$config = (object)array('layout' => 'list');
}
return $config;
}
return $config;
}
/**
* Maps the dimension data from DB to the form fields
*
* @param array $fields Array of dimensions definition as returned by {@link load_fields()}
* @return stdClass of fields data to be used by the mform set_data
*/
protected function prepare_form_fields(array $fields) {
/**
* Maps the dimension data from DB to the form fields
*
* @param array $fields Array of dimensions definition as returned by {@link load_fields()}
* @return stdClass of fields data to be used by the mform set_data
*/
protected function prepare_form_fields(array $fields) {
$formdata = new stdClass();
$key = 0;
foreach ($fields as $field) {
$formdata->{'dimensionid__idx_' . $key} = $field->id;
$formdata->{'description__idx_' . $key} = $field->description;
$formdata->{'description__idx_' . $key.'format'} = $field->descriptionformat;
$formdata->{'numoflevels__idx_' . $key} = count($field->levels);
$lev = 0;
foreach ($field->levels as $level) {
$formdata->{'levelid__idx_' . $key . '__idy_' . $lev} = $level->id;
$formdata->{'grade__idx_' . $key . '__idy_' . $lev} = $level->grade;
$formdata->{'definition__idx_' . $key . '__idy_' . $lev} = $level->definition;
$formdata->{'definition__idx_' . $key . '__idy_' . $lev . 'format'} = $level->definitionformat;
$formdata = new stdClass();
$key = 0;
foreach ($fields as $field) {
$formdata->{'dimensionid__idx_' . $key} = $field->id;
$formdata->{'description__idx_' . $key} = $field->description;
$formdata->{'description__idx_' . $key.'format'} = $field->descriptionformat;
$formdata->{'numoflevels__idx_' . $key} = count($field->levels);
$lev = 0;
foreach ($field->levels as $level) {
$formdata->{'levelid__idx_' . $key . '__idy_' . $lev} = $level->id;
$formdata->{'grade__idx_' . $key . '__idy_' . $lev} = $level->grade;
$formdata->{'definition__idx_' . $key . '__idy_' . $lev} = $level->definition;
$formdata->{'definition__idx_' . $key . '__idy_' . $lev . 'format'} = $level->definitionformat;
$lev++;
}
$key++;
@@ -563,67 +568,35 @@ protected function prepare_form_fields(array $fields) {
if (empty($grades)) {
return null;
}
// summarize the grades given in rubrics
$sumgrades = 0;
$sumweights = 0;
foreach ($grades as $grade) {
$dimension = $this->dimensions[$grade->dimensionid];
if ($dimension->weight < 0) {
throw new coding_exception('Negative weights are not supported any more. Something is wrong with your data');
}
if (grade_floats_equal($dimension->weight, 0) or grade_floats_equal($dimension->grade, 0)) {
// does not influence the final grade
continue;
}
if ($dimension->grade < 0) {
// this is a scale
$scaleid = -$dimension->grade;
$sumgrades += $this->scale_to_grade($scaleid, $grade->grade) * $dimension->weight * 100;
$sumweights += $dimension->weight;
} else {
// regular grade
$sumgrades += ($grade->grade / $dimension->grade) * $dimension->weight * 100;
$sumweights += $dimension->weight;
$sumgrades += $grade->grade;
}
// get the minimal and maximal possible grade (sum of minimal/maximal grades across all dimensions)
$mingrade = 0;
$maxgrade = 0;
foreach ($this->dimensions as $dimension) {
$mindimensiongrade = null;
$maxdimensiongrade = null;
foreach ($dimension->levels as $level) {
if (is_null($mindimensiongrade) or $level->grade < $mindimensiongrade) {
$mindimensiongrade = $level->grade;
}
if (is_null($maxdimensiongrade) or $level->grade > $maxdimensiongrade) {
$maxdimensiongrade = $level->grade;
}
}
$mingrade += $mindimensiongrade;
$maxgrade += $maxdimensiongrade;
}
if ($sumweights === 0) {
return 0;
if ($maxgrade - $mingrade > 0) {
return grade_floatval(100 * ($sumgrades - $mingrade) / ($maxgrade - $mingrade));
} else {
return null;
}
return grade_floatval($sumgrades / $sumweights);
}
/**
* Convert scale grade to numerical grades
*
* In rubric grading strategy, scales are considered as grades from 0 to M-1, where M is the number of scale items.
*
* @throws coding_exception
* @param string $scaleid Scale identifier
* @param int $item Selected scale item number, numbered 1, 2, 3, ... M
* @return float
*/
protected function scale_to_grade($scaleid, $item) {
global $DB;
/** @var cache of numbers of scale items */
static $numofscaleitems = array();
if (!isset($numofscaleitems[$scaleid])) {
$scale = $DB->get_field('scale', 'scale', array('id' => $scaleid), MUST_EXIST);
$items = explode(',', $scale);
$numofscaleitems[$scaleid] = count($items);
unset($scale);
unset($items);
}
if ($numofscaleitems[$scaleid] <= 1) {
throw new coding_exception('Invalid scale definition, no scale items found');
}
if ($item <= 0 or $numofscaleitems[$scaleid] < $item) {
throw new coding_exception('Invalid scale item number');
}
return ($item - 1) / ($numofscaleitems[$scaleid] - 1);
}
}
@@ -72,20 +72,47 @@ class workshop_rubric_strategy_test extends UnitTestCase {
$context = new stdClass();
$workshop = (object)array('id' => 42, 'strategy' => 'rubric');
$this->workshop = new workshop($workshop, $cm, $course, $context);
$DB->expectOnce('get_records_sql');
$DB->setReturnValue('get_records_sql', array());
$this->strategy = new testable_workshop_rubric_strategy($this->workshop);
// prepare dimensions definition
$dim = new stdClass();
$dim->id = 6;
$dim->levels[10] = (object)array('id' => 10, 'grade' => 0);
$dim->levels[13] = (object)array('id' => 13, 'grade' => 2);
$dim->levels[14] = (object)array('id' => 14, 'grade' => 6);
$dim->levels[16] = (object)array('id' => 16, 'grade' => 8);
$this->strategy->dimensions[$dim->id] = $dim;
$dim = new stdClass();
$dim->id = 8;
$dim->levels[17] = (object)array('id' => 17, 'grade' => 0);
$dim->levels[18] = (object)array('id' => 18, 'grade' => 1);
$dim->levels[19] = (object)array('id' => 19, 'grade' => 2);
$dim->levels[20] = (object)array('id' => 20, 'grade' => 3);
$this->strategy->dimensions[$dim->id] = $dim;
$dim = new stdClass();
$dim->id = 10;
$dim->levels[27] = (object)array('id' => 27, 'grade' => 10);
$dim->levels[28] = (object)array('id' => 28, 'grade' => 20);
$dim->levels[29] = (object)array('id' => 29, 'grade' => 30);
$dim->levels[30] = (object)array('id' => 30, 'grade' => 40);
$this->strategy->dimensions[$dim->id] = $dim;
}
public function tearDown() {
global $DB;
$DB = $this->realDB;
$this->workshop = null;
$this->strategy = null;
$this->workshop = null;
}
public function test_calculate_peer_grade_null_grade() {
// fixture set-up
$this->strategy->dimensions = array();
$grades = array();
// excercise SUT
$suggested = $this->strategy->calculate_peer_grade($grades);
@@ -93,4 +120,37 @@ class workshop_rubric_strategy_test extends UnitTestCase {
$this->assertNull($suggested);
}
public function test_calculate_peer_grade_worst_possible() {
// fixture set-up
$grades[6] = (object)array('dimensionid' => 6, 'grade' => 0);
$grades[8] = (object)array('dimensionid' => 8, 'grade' => 0);
$grades[10] = (object)array('dimensionid' => 10, 'grade' => 10);
// excercise SUT
$suggested = $this->strategy->calculate_peer_grade($grades);
// validate
$this->assertEqual(grade_floatval($suggested), 0.00000);
}
public function test_calculate_peer_grade_best_possible() {
// fixture set-up
$grades[6] = (object)array('dimensionid' => 6, 'grade' => 8);
$grades[8] = (object)array('dimensionid' => 8, 'grade' => 3);
$grades[10] = (object)array('dimensionid' => 10, 'grade' => 40);
// excercise SUT
$suggested = $this->strategy->calculate_peer_grade($grades);
// validate
$this->assertEqual(grade_floatval($suggested), 100.00000);
}
public function test_calculate_peer_grade_something() {
// fixture set-up
$grades[6] = (object)array('dimensionid' => 6, 'grade' => 2);
$grades[8] = (object)array('dimensionid' => 8, 'grade' => 2);
$grades[10] = (object)array('dimensionid' => 10, 'grade' => 30);
// excercise SUT
$suggested = $this->strategy->calculate_peer_grade($grades);
// validate
// minimal rubric score is 10, maximal is 51. We have 34 here
$this->assertEqual(grade_floatval($suggested), grade_floatval(100 * 24 / 41));
}
}
+21 -5
View File
@@ -412,13 +412,29 @@ function workshop_pluginfile($course, $cminfo, $context, $filearea, array $args,
'workshopform_rubric_description',
))) {
$itemid = (int)array_shift($args); // the id of the assessment form dimension
if (!$dimension = $DB->get_record('workshopform_numerrors', array('id' => $itemid))) {
send_file_not_found();
}
if (!$workshop = $DB->get_record('workshop', array('id' => $cminfo->instance))) {
send_file_not_found();
}
if ($workshop->id !== $dimension->workshopid) {
switch ($filearea) {
case 'workshopform_comments_description':
$dimension = $DB->get_record('workshopform_comments', array('id' => $itemid));
break;
case 'workshopform_accumulative_description':
$dimension = $DB->get_record('workshopform_accumulative', array('id' => $itemid));
break;
case 'workshopform_numerrors_description':
$dimension = $DB->get_record('workshopform_numerrors', array('id' => $itemid));
break;
case 'workshopform_rubric_description':
$dimension = $DB->get_record('workshopform_rubric', array('id' => $itemid));
break;
default:
$dimension = false;
}
if (empty($dimension)) {
send_file_not_found();
}
if ($workshop->id != $dimension->workshopid) {
// this should never happen but just in case
send_file_not_found();
}
@@ -434,7 +450,7 @@ function workshop_pluginfile($course, $cminfo, $context, $filearea, array $args,
send_stored_file($file);
}
if ($filearea === 'workshop_submission_content' or $filearea === 'workshop_submission_attachment') {
if ($filearea == 'workshop_submission_content' or $filearea == 'workshop_submission_attachment') {
$itemid = (int)array_shift($args);
if (!$submission = $DB->get_record('workshop_submissions', array('id' => $itemid))) {
return false;
+69
View File
@@ -326,6 +326,75 @@
margin: 0px 1em;
}
/* Rubric - list layout */
.mod-workshop .assessmentform.rubric.list .fitem .fitemtitle {
display: none;
}
.mod-workshop .assessmentform.rubric.list .fitem .felement {
width: auto;
}
.mod-workshop .assessmentform.rubric.list .fitem .felement span {
display: block;
}
.mod-workshop .assessmentform.rubric.list .fitem .felement span input {
display: block;
float: left;
}
.mod-workshop .assessmentform.rubric.list .fitem .felement.fgroup span label {
display: block;
margin-left: 30px;
}
/* Rubric - grid layout */
.mod-workshop .assessmentform.rubric #rubric-grid-wrapper {
border: none;
}
.mod-workshop .assessmentform.rubric #rubric-grid-wrapper legend {
display: none;
}
.mod-workshop .assessmentform.rubric.grid th,
.mod-workshop .assessmentform.rubric.grid td {
border: 1px solid #ddd;
padding: 5px;
vertical-align: top;
}
.mod-workshop .assessmentform.rubric.grid .criterion {
text-align: center;
}
.mod-workshop .assessmentform.rubric.grid .fitem {
text-align: center;
}
.mod-workshop .assessmentform.rubric.grid .fitem .fitemtitle {
display: none;
}
.mod-workshop .assessmentform.rubric.grid .fitem .felement {
width: 100%;
margin-left: auto;
margin-right: auto;
}
.mod-workshop .assessmentform.rubric.grid .fitem .felement span {
display: block;
text-align: center;
}
.mod-workshop .assessmentform.rubric.grid .fitem .felement span label {
display: block;
text-align: center;
}
/**
* Grading report
*/