From: David Mudrak Date: Mon, 4 Jan 2010 18:16:02 +0000 (+0000) Subject: MDL-19932 Rubric grading strategy implemented X-Git-Url: http://git.mjollnir.org/gw?a=commitdiff_plain;h=d524173efef74ab7f13cd5649b6fe490132502e2;p=moodle.git 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. --- diff --git a/mod/workshop/form/rubric/assessment_form.php b/mod/workshop/form/rubric/assessment_form.php index 2c6101e953..22ba1b1ad3 100644 --- a/mod/workshop/form/rubric/assessment_form.php +++ b/mod/workshop/form/rubric/assessment_form.php @@ -16,7 +16,10 @@ // along with Moodle. If not, see . /** - * 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 @@ -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 + */ +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_assessment_form extends workshop_assessment_form { +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"; $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); + $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, '', "
\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 { - // 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)); + /** + * 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', '' . "\n"); + $mform->addElement('html', ''); + $mform->addElement('html', ''); + + for ($i = 0; $i < $nodims; $i++) { + + $mform->addElement('html', '\n"; + $mform->addElement('html', $desc); + + $numoflevels = $fields->{'numoflevels__idx_'.$i}; + for ($j = 0; $j < $numoflevels; $j++) { + $colspan = $numofcolumns / $numoflevels; + $mform->addElement('html', '' . "\n"); + } + $mform->addElement('html', '' . "\n"); + } + $mform->addElement('html', '
' . get_string('criteria', 'workshopform_rubric') . ''.get_string('levels', 'workshopform_rubric').'
' . "\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 .= "' . "\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', '
' . "\n"); + $this->set_data($current); } } diff --git a/mod/workshop/form/rubric/edit_form.php b/mod/workshop/form/rubric/edit_form.php index f453224003..ca8ca35d03 100644 --- a/mod/workshop/form/rubric/edit_form.php +++ b/mod/workshop/form/rubric/edit_form.php @@ -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(); diff --git a/mod/workshop/form/rubric/lang/en_utf8/workshopform_rubric.php b/mod/workshop/form/rubric/lang/en_utf8/workshopform_rubric.php index ae64b218e3..5528ab5ec3 100644 --- a/mod/workshop/form/rubric/lang/en_utf8/workshopform_rubric.php +++ b/mod/workshop/form/rubric/lang/en_utf8/workshopform_rubric.php @@ -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'; diff --git a/mod/workshop/form/rubric/lib.php b/mod/workshop/form/rubric/lib.php index 760c831a10..347ce9460a 100644 --- a/mod/workshop/form/rubric/lib.php +++ b/mod/workshop/form/rubric/lib.php @@ -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; - - 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)); - - $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 + /** + * 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 - // 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}); - } + require_once(dirname(__FILE__) . '/edit_form.php'); - $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'); + $fields = $this->prepare_form_fields($this->dimensions); + $fields->config_layout = $this->config->layout; - return new workshop_edit_rubric_strategy_form($actionurl, $customdata, 'post', '', $attributes); -} + $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; + } -/** - * 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); + // 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); } - 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($record->id)) { - // new field - $record->id = $DB->insert_record('workshopform_rubric', $record); + /** + * 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); } - $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'); + /** + * 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); + $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}); + // 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}); - } + } - 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])) { - $current->{'gradeid__idx_'.$i} = $grades[$dimid]->id; - $current->{'grade__idx_'.$i} = $grades[$dimid]->grade; - $current->{'peercomment__idx_'.$i} = $grades[$dimid]->peercomment; + 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 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'); + // 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); - return new workshop_rubric_assessment_form($actionurl, $customdata, 'post', '', $attributes); -} + $formclassname = 'workshop_rubric_' . $this->config->layout . '_assessment_form'; + return new $formclassname($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; + /** + * 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'); - } - 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); - } else { - // updated grade - $DB->update_record('workshop_grades', $grade); + 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; + + 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); } - 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; + /** + * 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; } - 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; + /** + * 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; } - // all other evaluation methods are not supported yet - return false; -} -//////////////////////////////////////////////////////////////////////////////// -// Methods required by the 'best' evaluation plugin // -//////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////// + // 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'); - } + /** + * TODO + * + * @param resource $restrict + * @return TODO + */ + public function eval_best_get_assessments_recordset($restrict) { + global $DB; - $sql .= ' ORDER BY s.id'; // this is important for bulk processing + $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; - - $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 $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); + } } + 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; - - $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); - - $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(); + /** + * 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 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); + + $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) { - - $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; + /** + * 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; $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; - } - } - - if ($sumweights === 0) { - return 0; - } - 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); + $sumgrades += $grade->grade; } - if ($numofscaleitems[$scaleid] <= 1) { - throw new coding_exception('Invalid scale definition, no scale items found'); + // 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 ($item <= 0 or $numofscaleitems[$scaleid] < $item) { - throw new coding_exception('Invalid scale item number'); + if ($maxgrade - $mingrade > 0) { + return grade_floatval(100 * ($sumgrades - $mingrade) / ($maxgrade - $mingrade)); + } else { + return null; } - - return ($item - 1) / ($numofscaleitems[$scaleid] - 1); } } diff --git a/mod/workshop/form/rubric/simpletest/testlib.php b/mod/workshop/form/rubric/simpletest/testlib.php index ccc32224f8..8c5cb8e9a9 100644 --- a/mod/workshop/form/rubric/simpletest/testlib.php +++ b/mod/workshop/form/rubric/simpletest/testlib.php @@ -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)); + } } diff --git a/mod/workshop/lib.php b/mod/workshop/lib.php index 2fd3b83d6d..804a3ab6af 100644 --- a/mod/workshop/lib.php +++ b/mod/workshop/lib.php @@ -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))) { + if (!$workshop = $DB->get_record('workshop', array('id' => $cminfo->instance))) { send_file_not_found(); } - if (!$workshop = $DB->get_record('workshop', array('id' => $cminfo->instance))) { + 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) { + 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; diff --git a/mod/workshop/styles.php b/mod/workshop/styles.php index 7c7d04d923..2dac650363 100644 --- a/mod/workshop/styles.php +++ b/mod/workshop/styles.php @@ -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 */