From 669314615fdb61ad6bbe190d4656f0bf7cbee184 Mon Sep 17 00:00:00 2001 From: David Mudrak Date: Mon, 4 Jan 2010 18:21:04 +0000 Subject: [PATCH] Initial copy of form/accumulative code --- .../form/comments/assessment_form.php | 80 +++ mod/workshop/form/comments/edit_form.php | 77 +++ .../en_utf8/workshopform_accumulative.php | 35 ++ mod/workshop/form/comments/lib.php | 509 ++++++++++++++++++ .../form/comments/simpletest/testlib.php | 230 ++++++++ mod/workshop/form/comments/version.php | 32 ++ 6 files changed, 963 insertions(+) create mode 100644 mod/workshop/form/comments/assessment_form.php create mode 100644 mod/workshop/form/comments/edit_form.php create mode 100644 mod/workshop/form/comments/lang/en_utf8/workshopform_accumulative.php create mode 100644 mod/workshop/form/comments/lib.php create mode 100644 mod/workshop/form/comments/simpletest/testlib.php create mode 100644 mod/workshop/form/comments/version.php diff --git a/mod/workshop/form/comments/assessment_form.php b/mod/workshop/form/comments/assessment_form.php new file mode 100644 index 0000000000..25d6a7e80d --- /dev/null +++ b/mod/workshop/form/comments/assessment_form.php @@ -0,0 +1,80 @@ +. + +/** + * This file defines an mform to assess a submission by accumulative grading strategy + * + * @package mod-workshop + * @copyright 2009 David Mudrak + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once(dirname(dirname(__FILE__)).'/assessment_form.php'); // parent class definition + +/** + * Class representing a form for assessing submissions by accumulative grading strategy + * + * @uses moodleform + */ +class workshop_accumulative_assessment_form extends workshop_assessment_form { + + /** + * Define the elements to be displayed at the form + * + * Called by the parent::definition() + * + * @return void + */ + protected function definition_inner(&$mform) { + $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_accumulative', $i+1); + $mform->addElement('header', 'dimensionhdr__idx_'.$i, $dimtitle); + + // dimension id + $mform->addElement('hidden', 'dimensionid__idx_'.$i, $fields->{'dimensionid__idx_'.$i}); + + // grade id + $mform->addElement('hidden', 'gradeid__idx_'.$i); // value set by set_data() later + + // dimension description + $desc = '
'."\n"; + $desc .= format_text($fields->{'description__idx_'.$i}, $fields->{'description__idx_'.$i.'format'}); + $desc .= "\n
"; + $mform->addElement('html', $desc); + + // grade for this aspect + $label = get_string('dimensiongrade', 'workshopform_accumulative'); + $options = make_grades_menu($fields->{'grade__idx_' . $i}); + $mform->addElement('select', 'grade__idx_' . $i, $label, $options); + + // comment + $label = get_string('dimensioncomment', 'workshopform_accumulative'); + //$mform->addElement('editor', 'peercomment__idx_' . $i, $label, null, array('maxfiles' => 0)); + $mform->addElement('textarea', 'peercomment__idx_' . $i, $label, array('cols' => 60, 'rows' => 5)); + } + $this->set_data($current); + } +} diff --git a/mod/workshop/form/comments/edit_form.php b/mod/workshop/form/comments/edit_form.php new file mode 100644 index 0000000000..41a55d686e --- /dev/null +++ b/mod/workshop/form/comments/edit_form.php @@ -0,0 +1,77 @@ +. + +/** + * This file defines an mform to edit accumulative grading strategy forms. + * + * @package mod-workshop + * @copyright 2009 David Mudrak + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once(dirname(dirname(dirname(__FILE__))).'/lib.php'); // module library +require_once(dirname(dirname(__FILE__)).'/edit_form.php'); // parent class definition + +/** + * Class for editing accumulative grading strategy forms. + * + * @uses moodleform + */ +class workshop_edit_accumulative_strategy_form extends workshop_edit_strategy_form { + + /** + * Define the elements to be displayed at the form + * + * Called by the parent::definition() + * + * @return void + */ + protected function definition_inner(&$mform) { + + $norepeats = $this->_customdata['norepeats']; // number of dimensions to display + $descriptionopts = $this->_customdata['descriptionopts']; // wysiwyg fields options + $current = $this->_customdata['current']; // current data to be set + + $mform->addElement('hidden', 'norepeats', $norepeats); + // value not to be overridden by submitted value + $mform->setConstants(array('norepeats' => $norepeats)); + + $weights = workshop_get_dimension_weights(); + + for ($i = 0; $i < $norepeats; $i++) { + $mform->addElement('header', 'dimension'.$i, get_string('dimensionnumber', 'workshopform_accumulative', $i+1)); + $mform->addElement('hidden', 'dimensionid__idx_'.$i); + $mform->addElement('editor', 'description__idx_'.$i.'_editor', + get_string('dimensiondescription', 'workshopform_accumulative'), '', $descriptionopts); + // todo replace modgrade with an advanced element (usability issue discussed with Olli) + $mform->addElement('modgrade', 'grade__idx_'.$i, + get_string('dimensionmaxgrade','workshopform_accumulative'), null, true); + $mform->setDefault('grade__idx_'.$i, 10); + $mform->addElement('select', 'weight__idx_'.$i, + get_string('dimensionweight', 'workshopform_accumulative'), $weights); + $mform->setDefault('weight__idx_'.$i, 1); + } + + $mform->registerNoSubmitButton('noadddims'); + $mform->addElement('submit', 'noadddims', get_string('addmoredimensions', 'workshopform_accumulative', + workshop_accumulative_strategy::ADDDIMS)); + $mform->closeHeaderBefore('noadddims'); + $this->set_data($current); + } +} diff --git a/mod/workshop/form/comments/lang/en_utf8/workshopform_accumulative.php b/mod/workshop/form/comments/lang/en_utf8/workshopform_accumulative.php new file mode 100644 index 0000000000..7dbcd96f6e --- /dev/null +++ b/mod/workshop/form/comments/lang/en_utf8/workshopform_accumulative.php @@ -0,0 +1,35 @@ +. + +/** + * English strings for Workshop subplugin - "Accumulative" grading strategy + * + * @package mod-workshop + * @copyright 2009 David Mudrak + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$string['addmoredimensions'] = 'Blanks for $a more aspects'; +$string['dimensioncomment'] = 'Comment'; +$string['dimensiondescription'] = 'Description'; +$string['dimensiongrade'] = 'Grade'; +$string['dimensionmaxgrade'] = 'Best possible grade / Scale to use'; +$string['dimensionnumber'] = 'Aspect $a'; +$string['dimensionweight'] = 'Weight'; +$string['pluginname'] = 'Accumulative grading'; diff --git a/mod/workshop/form/comments/lib.php b/mod/workshop/form/comments/lib.php new file mode 100644 index 0000000000..6fd2cde4e3 --- /dev/null +++ b/mod/workshop/form/comments/lib.php @@ -0,0 +1,509 @@ +. + +/** + * This file defines a class with accumulative grading strategy logic + * + * @package mod-workshop + * @copyright 2009 David Mudrak + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once(dirname(dirname(__FILE__)) . '/lib.php'); // interface definition +require_once($CFG->libdir . '/gradelib.php'); // to handle float vs decimal issues + +/** + * Accumulative grading strategy logic. + */ +class workshop_accumulative_strategy implements workshop_strategy { + + /** @const default number of dimensions to show */ + const MINDIMS = 3; + + /** @const number of dimensions to add */ + const ADDDIMS = 2; + + /** @var workshop the parent workshop instance */ + protected $workshop; + + /** @var array definition of the assessment form fields */ + protected $dimensions = null; + + /** @var array options for dimension description fields */ + protected $descriptionopts; + + /** + * Constructor + * + * @param workshop $workshop The workshop instance record + * @return void + */ + public function __construct(workshop $workshop) { + $this->workshop = $workshop; + $this->dimensions = $this->load_fields(); + $this->descriptionopts = 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 $PAGE; + + require_once(dirname(__FILE__) . '/edit_form.php'); + + $fields = $this->prepare_form_fields($this->dimensions); + $nodimensions = count($this->dimensions); + $norepeatsdefault = max($nodimensions + self::ADDDIMS, self::MINDIMS); + $norepeats = optional_param('norepeats', $norepeatsdefault, PARAM_INT); // number of dimensions + $noadddims = optional_param('noadddims', '', PARAM_ALPHA); // shall we add more? + if ($noadddims) { + $norepeats += self::ADDDIMS; + } + + // 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, + $PAGE->context, 'workshopform_accumulative_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_accumulative_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, $PAGE; + + $workshopid = $data->workshopid; + $norepeats = $data->norepeats; + + $data = $this->prepare_database_fields($data); + $records = $data->accumulative; // records to be saved into {workshopform_accumulative} + $todelete = array(); // dimension ids to be deleted + + for ($i=0; $i < $norepeats; $i++) { + $record = $records[$i]; + if (0 == strlen(trim($record->description_editor['text']))) { + if (!empty($record->id)) { + // existing record with empty description - to be deleted + $todelete[] = $record->id; + } + continue; + } + if (empty($record->id)) { + // new field + $record->id = $DB->insert_record('workshopform_accumulative', $record); + } else { + // exiting field + $DB->update_record('workshopform_accumulative', $record); + } + // re-save with correct path to embeded media files + $record = file_postupdate_standard_editor($record, 'description', $this->descriptionopts, + $PAGE->context, 'workshopform_accumulative_description', $record->id); + $DB->update_record('workshopform_accumulative', $record); + } + $this->delete_dimensions($todelete); + } + + /** + * 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 + * @param stdClass $assessment The current assessment + * @param bool $editable + */ + public function get_assessment_form(moodle_url $actionurl=null, $mode='preview', stdClass $assessment=null, $editable=true) { + global $CFG; // needed because the included files use it + global $PAGE; + 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', $PAGE->context->id, 'workshopform_accumulative_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; + } + } + } + + // 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 accumulative'); + + return new workshop_accumulative_assessment_form($actionurl, $customdata, 'post', '', $attributes, $editable); + } + + /** + * 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 = 'accumulative'; + $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); + } + } + 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; + } + + /** + * @see parent::get_assessments_recordset() + */ + public function get_assessments_recordset($restrict=null) { + 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'); + } + + $sql .= ' ORDER BY s.id'; // this is important for bulk processing + + return $DB->get_recordset_sql($sql, $params); + } + + /** + * @see parent::get_dimensions_info() + */ + public function get_dimensions_info() { + global $DB; + + $sql = 'SELECT d.id, d.grade, d.weight, s.scale + FROM {workshopform_accumulative} 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; + } + + //////////////////////////////////////////////////////////////////////////////// + // 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 * + FROM {workshopform_accumulative} + WHERE workshopid = :workshopid + ORDER BY sort'; + $params = array('workshopid' => $this->workshop->id); + + return $DB->get_records_sql($sql, $params); + } + + /** + * Maps the dimension data from DB to the form fields + * + * @param array $raw Array of raw dimension records as returned by {@link load_fields()} + * @return array Array of fields data to be used by the mform set_data + */ + protected function prepare_form_fields(array $raw) { + + $formdata = new stdClass(); + $key = 0; + foreach ($raw as $dimension) { + $formdata->{'dimensionid__idx_' . $key} = $dimension->id; + $formdata->{'description__idx_' . $key} = $dimension->description; + $formdata->{'description__idx_' . $key.'format'} = $dimension->descriptionformat; + $formdata->{'grade__idx_' . $key} = $dimension->grade; + $formdata->{'weight__idx_' . $key} = $dimension->weight; + $key++; + } + return $formdata; + } + + /** + * Deletes dimensions and removes embedded media from its descriptions + * + * todo we may check that there are no assessments done using these dimensions and probably remove them + * + * @param array $masterids + * @return void + */ + protected function delete_dimensions(array $ids) { + global $DB, $PAGE; + + $fs = get_file_storage(); + foreach ($ids as $id) { + if (!empty($id)) { // to prevent accidental removal of all files in the area + $fs->delete_area_files($PAGE->context->id, 'workshopform_accumulative_description', $id); + } + } + $DB->delete_records_list('workshopform_accumulative', 'id', $ids); + } + + /** + * Prepares data returned by {@link workshop_edit_accumulative_strategy_form} so they can be saved into database + * + * It automatically adds some columns into every record. The sorting is + * done by the order of the returned array and starts with 1. + * Called internally from {@link save_edit_strategy_form()} only. Could be private but + * keeping protected for unit testing purposes. + * + * @param stdClass $raw Raw data returned by mform + * @return array Array of objects to be inserted/updated in DB + */ + protected function prepare_database_fields(stdClass $raw) { + global $PAGE; + + $cook = new stdClass(); // to be returned + $cook->accumulative = array(); // records to be stored in {workshopform_accumulative} + + for ($i = 0; $i < $raw->norepeats; $i++) { + $cook->accumulative[$i] = new stdClass(); + $cook->accumulative[$i]->id = $raw->{'dimensionid__idx_'.$i}; + $cook->accumulative[$i]->workshopid = $this->workshop->id; + $cook->accumulative[$i]->sort = $i + 1; + $cook->accumulative[$i]->description_editor = $raw->{'description__idx_'.$i.'_editor'}; + $cook->accumulative[$i]->grade = $raw->{'grade__idx_'.$i}; + $cook->accumulative[$i]->weight = $raw->{'weight__idx_'.$i}; + } + return $cook; + } + + /** + * Returns the list of current grades filled by the reviewer indexed by dimensionid + * + * @param stdClass $assessment Assessment record + * @return array [int dimensionid] => stdClass workshop_grades record + */ + protected function get_current_assessment_data(stdClass $assessment) { + global $DB; + + if (empty($this->dimensions)) { + return array(); + } + list($dimsql, $dimparams) = $DB->get_in_or_equal(array_keys($this->dimensions), SQL_PARAMS_NAMED); + // beware! the caller may rely on the returned array is indexed by dimensionid + $sql = "SELECT dimensionid, * + FROM {workshop_grades} + WHERE assessmentid = :assessmentid AND strategy= :strategy AND dimensionid $dimsql"; + $params = array('assessmentid' => $assessment->id, 'strategy' => 'accumulative'); + $params = array_merge($params, $dimparams); + + return $DB->get_records_sql($sql, $params); + } + + /** + * Aggregates the assessment form data and sets the grade for the submission given by the peer + * + * @param stdClass $assessment Assessment record + * @return float|null Raw grade (from 0.00000 to 100.00000) for submission as suggested by the peer + */ + protected function update_peer_grade(stdClass $assessment) { + $grades = $this->get_current_assessment_data($assessment); + $suggested = $this->calculate_peer_grade($grades); + if (!is_null($suggested)) { + $this->workshop->set_peer_grade($assessment->id, $suggested); + } + return $suggested; + } + + /** + * Calculates the aggregated grade given by the reviewer + * + * @param array $grades Grade records as returned by {@link get_current_assessment_data} + * @uses $this->dimensions + * @return float|null Raw grade (from 0.00000 to 100.00000) for submission as suggested by the peer + */ + protected function calculate_peer_grade(array $grades) { + + if (empty($grades)) { + return null; + } + $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 accumulative 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/comments/simpletest/testlib.php b/mod/workshop/form/comments/simpletest/testlib.php new file mode 100644 index 0000000000..34e6b219b6 --- /dev/null +++ b/mod/workshop/form/comments/simpletest/testlib.php @@ -0,0 +1,230 @@ +. + +/** + * Unit tests for Accumulative grading strategy logic + * + * @package mod-workshop + * @copyright 2009 David Mudrak + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +// Include the code to test +require_once($CFG->dirroot . '/mod/workshop/locallib.php'); +require_once($CFG->dirroot . '/mod/workshop/form/accumulative/lib.php'); + +global $DB; +Mock::generate(get_class($DB), 'mockDB'); + +/** + * Test subclass that makes all the protected methods we want to test public + */ +class testable_workshop_accumulative_strategy extends workshop_accumulative_strategy { + + /** allows to set dimensions manually */ + public $dimensions = array(); + + /** + * This is where the calculation of suggested grade for submission is done + */ + public function calculate_peer_grade(array $grades) { + return parent::calculate_peer_grade($grades); + } +} + +class workshop_accumulative_strategy_test extends UnitTestCase { + + /** real database */ + protected $realDB; + + /** workshop instance emulation */ + protected $workshop; + + /** instance of the strategy logic class being tested */ + protected $strategy; + + /** + * Setup testing environment + */ + public function setUp() { + global $DB; + $this->realDB = $DB; + $DB = new mockDB(); + + $cm = new stdClass(); + $course = new stdClass(); + $context = new stdClass(); + $workshop = (object)array('id' => 42, 'strategy' => 'accumulative'); + $this->workshop = new workshop($workshop, $cm, $course, $context); + $this->strategy = new testable_workshop_accumulative_strategy($this->workshop); + } + + public function tearDown() { + global $DB; + $DB = $this->realDB; + + $this->workshop = null; + $this->strategy = 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); + // validate + $this->assertNull($suggested); + } + + public function test_calculate_peer_grade_one_numerical() { + // fixture set-up + $this->strategy->dimensions[1003] = (object)array('grade' => '20', 'weight' => '1'); + $grades[] = (object)array('dimensionid' => 1003, 'grade' => '5.00000'); + // excercise SUT + $suggested = $this->strategy->calculate_peer_grade($grades); + // validate + $this->assertEqual(grade_floatval(5/20 * 100), $suggested); + } + + public function test_calculate_peer_grade_negative_weight() { + // fixture set-up + $this->strategy->dimensions[1003] = (object)array('grade' => '20', 'weight' => '-1'); + $grades[] = (object)array('dimensionid' => 1003, 'grade' => '20'); + $this->expectException('coding_exception'); + // excercise SUT + $suggested = $this->strategy->calculate_peer_grade($grades); + } + + public function test_calculate_peer_grade_one_numerical_weighted() { + // fixture set-up + $this->strategy->dimensions[1003] = (object)array('grade' => '20', 'weight' => '3'); + $grades[] = (object)array('dimensionid' => '1003', 'grade' => '5'); + // excercise SUT + $suggested = $this->strategy->calculate_peer_grade($grades); + // validate + $this->assertEqual(grade_floatval(5/20 * 100), $suggested); + } + + public function test_calculate_peer_grade_three_numericals_same_weight() { + // fixture set-up + $this->strategy->dimensions[1003] = (object)array('grade' => '20', 'weight' => '2'); + $this->strategy->dimensions[1004] = (object)array('grade' => '100', 'weight' => '2'); + $this->strategy->dimensions[1005] = (object)array('grade' => '10', 'weight' => '2'); + + $grades[] = (object)array('dimensionid' => 1003, 'grade' => '11.00000'); + $grades[] = (object)array('dimensionid' => 1004, 'grade' => '87.00000'); + $grades[] = (object)array('dimensionid' => 1005, 'grade' => '10.00000'); + + // excercise SUT + $suggested = $this->strategy->calculate_peer_grade($grades); + + // validate + $this->assertEqual(grade_floatval((11/20 + 87/100 + 10/10)/3 * 100), $suggested); + } + + public function test_calculate_peer_grade_three_numericals_different_weights() { + // fixture set-up + $this->strategy->dimensions[1003] = (object)array('grade' => '15', 'weight' => 3); + $this->strategy->dimensions[1004] = (object)array('grade' => '80', 'weight' => 1); + $this->strategy->dimensions[1005] = (object)array('grade' => '5', 'weight' => 2); + + $grades[] = (object)array('dimensionid' => 1003, 'grade' => '7.00000'); + $grades[] = (object)array('dimensionid' => 1004, 'grade' => '66.00000'); + $grades[] = (object)array('dimensionid' => 1005, 'grade' => '4.00000'); + + // excercise SUT + $suggested = $this->strategy->calculate_peer_grade($grades); + + // validate + $this->assertEqual(grade_floatval((7/15*3 + 66/80*1 + 4/5*2)/6 * 100), $suggested); + } + + public function test_calculate_peer_grade_one_scale_max() { + global $DB; + + // fixture set-up + $mockscale = 'E,D,C,B,A'; + $this->strategy->dimensions[1008] = (object)array('grade' => '-10', 'weight' => 1); + $grades[] = (object)array('dimensionid' => 1008, 'grade' => '5.00000'); + $DB->expectOnce('get_field', array('scale', 'scale', array('id' => 10), MUST_EXIST)); + $DB->setReturnValue('get_field', $mockscale); + + // excercise SUT + $suggested = $this->strategy->calculate_peer_grade($grades); + + // validate + $this->assertEqual(100.00000, $suggested); + } + + public function test_calculate_peer_grade_one_scale_min_with_scale_caching() { + global $DB; + + // fixture set-up + $this->strategy->dimensions[1008] = (object)array('grade' => '-10', 'weight' => 1); + $grades[] = (object)array('dimensionid' => 1008, 'grade' => '1.00000'); + $DB->expectNever('get_field', array('scale', 'scale', array('id' => 10), MUST_EXIST)); // cached + + // excercise SUT + $suggested = $this->strategy->calculate_peer_grade($grades); + + // validate + $this->assertEqual(0.00000, $suggested); + } + + public function test_calculate_peer_grade_two_scales_weighted() { + global $DB; + + // fixture set-up + $mockscale13 = 'Poor,Good,Excellent'; + $mockscale17 = '-,*,**,***,****,*****,******'; + + $this->strategy->dimensions[1012] = (object)array('grade' => '-13', 'weight' => 2); + $this->strategy->dimensions[1019] = (object)array('grade' => '-17', 'weight' => 3); + + $grades[] = (object)array('dimensionid' => 1012, 'grade' => '2.00000'); // "Good" + $grades[] = (object)array('dimensionid' => 1019, 'grade' => '5.00000'); // "****" + + $DB->expectAt(0, 'get_field', array('scale', 'scale', array('id' => 13), MUST_EXIST)); + $DB->setReturnValueAt(0, 'get_field', $mockscale13); + + $DB->expectAt(1, 'get_field', array('scale', 'scale', array('id' => 17), MUST_EXIST)); + $DB->setReturnValueAt(1, 'get_field', $mockscale17); + + // excercise SUT + $suggested = $this->strategy->calculate_peer_grade($grades); + + // validate + $this->assertEqual(grade_floatval((1/2*2 + 4/6*3)/5 * 100), $suggested); + } + + public function test_calculate_peer_grade_scale_exception() { + global $DB; + + // fixture set-up + $mockscale13 = 'Poor,Good,Excellent'; + $this->strategy->dimensions[1012] = (object)array('grade' => -13, 'weight' => 1); + $DB->expectNever('get_field', array('scale', 'scale', array('id' => 13), MUST_EXIST)); // cached + $grades[] = (object)array('dimensionid' => 1012, 'grade' => '4.00000'); // exceeds the number of scale items + $this->expectException('coding_exception'); + + // excercise SUT + $suggested = $this->strategy->calculate_peer_grade($grades); + } +} diff --git a/mod/workshop/form/comments/version.php b/mod/workshop/form/comments/version.php new file mode 100644 index 0000000000..3966260003 --- /dev/null +++ b/mod/workshop/form/comments/version.php @@ -0,0 +1,32 @@ +. + +/** + * Defines the version of workshop accumulative grading strategy subplugin + * + * This code fragment is called by moodle_needs_upgrading() and + * /admin/index.php + * + * @package mod-workshop + * @copyright 2009 David Mudrak + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2009081300; +$plugin->requires = 2009080700; // Requires this Moodle version -- 2.39.5