diff --git a/mod/workshop/form/rubric/assessment_form.php b/mod/workshop/form/rubric/assessment_form.php index 2c6101e9537..22ba1b1ad34 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 */ -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"; $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, '', "
\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', '' . "\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 f4532240035..ca8ca35d03b 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 ae64b218e33..5528ab5ec30 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 760c831a108..347ce9460aa 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; + /** + * 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); } } diff --git a/mod/workshop/form/rubric/simpletest/testlib.php b/mod/workshop/form/rubric/simpletest/testlib.php index ccc32224f86..8c5cb8e9a9b 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 2fd3b83d6d0..804a3ab6af3 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))) { - 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; diff --git a/mod/workshop/styles.php b/mod/workshop/styles.php index 7c7d04d9234..2dac6503632 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 */