From 46abdfc86ea339da9f2060436d4db799dceecaac Mon Sep 17 00:00:00 2001 From: Gordon Bateson Date: Fri, 1 Jan 2016 21:44:08 +0900 Subject: [PATCH] MDL-79863 qtype_ordering: qtype_ordering fix grade calculation for RELATIVE_ONE_PREVIOUS_AND_NEXT and RELATIVE_ALL_PREVIOUS_AND_NEXT, and add grade/score details for less than perfect grades --- .../type/ordering/lang/en/qtype_ordering.php | 2 + question/type/ordering/question.php | 12 +- question/type/ordering/renderer.php | 277 ++++++++++++------ question/type/ordering/styles.css | 12 +- question/type/ordering/version.php | 4 +- 5 files changed, 205 insertions(+), 102 deletions(-) diff --git a/question/type/ordering/lang/en/qtype_ordering.php b/question/type/ordering/lang/en/qtype_ordering.php index dc03b1f049e..45fcdd3622d 100644 --- a/question/type/ordering/lang/en/qtype_ordering.php +++ b/question/type/ordering/lang/en/qtype_ordering.php @@ -21,6 +21,7 @@ $string['answer'] = 'Item text'; $string['answerheader'] = 'Draggable item {no}'; $string['correctorder'] = 'The correct order for these items is as follows:'; $string['defaultquestionname'] = 'Drag the following items into the correct order.'; +$string['gradedetails'] = 'Grade details'; $string['gradingtype'] = 'Grading type'; $string['gradingtype_help'] = 'Choose the type of grading calculation. @@ -49,6 +50,7 @@ $string['relativenextincludelast'] = 'Relative to the next item (including last) $string['relativeonepreviousandnext'] = 'Relative to both the previous and next items'; $string['removeeditor'] = 'Remove HTML editor'; $string['removeitem'] = 'Remove draggable item'; +$string['scoredetails'] = 'Here are the scores for each item in this response:'; $string['selectall'] = 'Select all items'; $string['selectcontiguous'] = 'Select a contiguous subset of items'; $string['selectcount_help'] = 'The number of items that will be displayed when the question is appears in a quiz.'; diff --git a/question/type/ordering/question.php b/question/type/ordering/question.php index 72d8fca8056..5cf99bbfa44 100644 --- a/question/type/ordering/question.php +++ b/question/type/ordering/question.php @@ -190,18 +190,14 @@ class qtype_ordering_question extends question_graded_automatically { foreach ($correctresponse as $thisanswerid => $answerids) { if (isset($currentresponse[$thisanswerid])) { $prev = $currentresponse[$thisanswerid]->prev; - $prev = array_intersect($answerids->prev, $prev); + $prev = array_intersect($prev, $answerids->prev); $countcorrect += count($prev); $next = $currentresponse[$thisanswerid]->next; - $next = array_intersect($answerids->next, $next); + $next = array_intersect($next, $answerids->next); $countcorrect += count($next); } - $countanswers++; - } - if ($options->gradingtype) { - $countanswers *= 2; - } else { - $countanswers *= (count($correctresponse) - 1); + $countanswers += count($answerids->prev); + $countanswers += count($answerids->next); } break; } diff --git a/question/type/ordering/renderer.php b/question/type/ordering/renderer.php index bcb3501301d..e139463ed48 100644 --- a/question/type/ordering/renderer.php +++ b/question/type/ordering/renderer.php @@ -38,6 +38,10 @@ if (! class_exists('qtype_with_combined_feedback_renderer')) { // Moodle 2.0 */ class qtype_ordering_renderer extends qtype_with_combined_feedback_renderer { + protected $correctinfo = null; + protected $currentinfo = null; + protected $itemscores = array(); + public function formulation_and_controls(question_attempt $qa, question_display_options $options) { global $CFG, $DB; @@ -102,31 +106,9 @@ class qtype_ordering_renderer extends qtype_with_combined_feedback_renderer { // set layout class $layoutclass = $question->get_ordering_layoutclass(); - // get info about current/correct responses - if ($options->correctness) { - switch ($question->options->gradingtype) { - - case 0: // ABSOLUTE - $correctinfo = $correctresponse; - $currentinfo = $currentresponse; - break; - - case 1: // RELATIVE_NEXT_EXCLUDE_LAST - case 2: // RELATIVE_NEXT_INCLUDE_LAST - $currentinfo = $question->get_next_answerids($currentresponse, ($question->options->gradingtype==2)); - $correctinfo = $question->get_next_answerids($correctresponse, ($question->options->gradingtype==2)); - break; - - case 3: // RELATIVE_ONE_PREVIOUS_AND_NEXT - case 4: // RELATIVE_ALL_PREVIOUS_AND_NEXT - $currentinfo = $question->get_previous_and_next_answerids($currentresponse, ($question->options->gradingtype==4)); - $correctinfo = $question->get_previous_and_next_answerids($correctresponse, ($question->options->gradingtype==4)); - break; - } - } - // generate ordering items foreach ($currentresponse as $position => $answerid) { + if (! array_key_exists($answerid, $question->answers)) { continue; // shouldn't happen !! } @@ -141,71 +123,13 @@ class qtype_ordering_renderer extends qtype_with_combined_feedback_renderer { $result .= html_writer::start_tag('ul', array('class' => 'sortablelist', 'id' => $sortable_id)); } - // CSS $class and $img are only used to show correctness - $class = ''; - $img = ''; - - // display the correctness of this item + // set the CSS class and correctness img for this response if ($options->correctness) { - - // correctness depends on grading type - $score = 0; // actual score for this item - $maxscore = null; // maximum score for this item - switch ($question->options->gradingtype) { - - case 0: // ABSOLUTE - if (isset($correctinfo[$position])) { - if ($correctinfo[$position]==$answerid) { - $score = 1; - } - $maxscore = 1; - } - break; - - case 1; // RELATIVE_NEXT_EXCLUDE_LAST - case 2; // RELATIVE_NEXT_INCLUDE_LAST - if (isset($correctinfo[$answerid])) { - if (isset($currentinfo[$answerid]) && $currentinfo[$answerid]==$correctinfo[$answerid]) { - $score = 1; - } - $maxscore = 1; - } - break; - - case 3; // RELATIVE_ONE_PREVIOUS_AND_NEXT - case 4; // RELATIVE_ALL_PREVIOUS_AND_NEXT - if (isset($correctinfo[$answerid])) { - $maxscore = 0; - $prev = $correctinfo[$answerid]->prev; - $maxscore += count($prev); - $prev = array_intersect($prev, $currentinfo[$answerid]->prev); - $score += count($prev); - $next = $correctinfo[$answerid]->next; - $maxscore += count($next); - $next = array_intersect($next, $currentinfo[$answerid]->next); - $score += count($next); - } - break; - } - if ($maxscore===null) { - $class = 'unscored'; - } else { - if ($maxscore==0) { - $score = 0.0; - } else { - $score = ($score / $maxscore); - } - switch (true) { - case ($score > 0.999999): $class = 'correct'; break; - case ($score < 0.000001): $class = 'incorrect'; break; - case ($score >= 0.66): $class = 'partial66'; break; - case ($score >= 0.33): $class = 'partial33'; break; - default: $class = 'partial01'; break; - } - $img = $this->feedback_image($score).' '; - } + $score = $this->get_ordering_item_score($question, $position, $answerid); + list($score, $maxscore, $fraction, $percent, $class, $img) = $score; } else { $class = 'sortableitem'; + $img = ''; } $class = "$class $layoutclass"; @@ -236,7 +160,73 @@ class qtype_ordering_renderer extends qtype_with_combined_feedback_renderer { } public function specific_feedback(question_attempt $qa) { - return $this->combined_feedback($qa); + + if ($feedback = $this->combined_feedback($qa)) { + $feedback = html_writer::tag('p', $feedback); + } + + $gradingtype = ''; + $gradedetails = ''; + $scoredetails = ''; + + // if required, add explanation of grade calculation + if ($step = $qa->get_last_step()) { + $state = $step->get_state(); + if ($state=='gradedpartial' || $state=='gradedwrong') { + + $plugin = 'qtype_ordering'; + $question = $qa->get_question(); + + // fetch grading type + switch ($question->options->gradingtype) { + case 0: $gradingtype = get_string('absoluteposition', $plugin); break; + case 1: $gradingtype = get_string('relativenextexcludelast', $plugin); break; + case 2: $gradingtype = get_string('relativenextincludelast', $plugin); break; + case 3: $gradingtype = get_string('relativeonepreviousandnext', $plugin); break; + case 4: $gradingtype = get_string('relativeallpreviousandnext', $plugin); break; + } + + // format grading type, e.g. Grading type: Relative to next item, excluding last item + if ($gradingtype) { + $gradingtype = get_string('gradingtype', $plugin).': '.$gradingtype; + $gradingtype = html_writer::tag('p', $gradingtype, array('class' => 'gradingtype')); + } + + // fetch grade details and score details + if ($currentresponse = $question->currentresponse) { + + $totalscore = 0; + $totalmaxscore = 0; + + $layoutclass = $question->get_ordering_layoutclass(); + $params = array('class' => $layoutclass); + + $scoredetails .= html_writer::tag('p', get_string('scoredetails', $plugin)); + $scoredetails .= html_writer::start_tag('ol', array('class' => 'scoredetails')); + + // format scoredetails, e.g. 1 /2 = 50%, for each item + foreach ($currentresponse as $position => $answerid) { + $answer = $question->answers[$answerid]; + $score = $this->get_ordering_item_score($question, $position, $answerid); + list($score, $maxscore, $fraction, $percent, $class, $img) = $score; + $totalscore += $score; + $totalmaxscore += $maxscore; + $score = "$score / $maxscore = $percent%"; + $scoredetails .= html_writer::tag('li', $score, $params); + } + + $scoredetails .= html_writer::end_tag('ol'); + + // format gradedetails, e.g. 4 /6 = 67% + $gradedetails = round(100 * $totalscore / $totalmaxscore, 0); + $gradedetails = "$totalscore / $totalmaxscore = $gradedetails%"; + $gradedetails = get_string('gradedetails', $plugin).': '.$gradedetails; + $gradedetails = html_writer::tag('p', $gradedetails, array('class' => 'gradedetails')); + } + } + } + + return $feedback.$gradingtype.$gradedetails.$scoredetails; } public function correct_response(question_attempt $qa) { @@ -271,4 +261,119 @@ class qtype_ordering_renderer extends qtype_with_combined_feedback_renderer { return $output; } + + ///////////////////////////////////// + // custom methods + ///////////////////////////////////// + + protected function get_response_info($question) { + switch ($question->options->gradingtype) { + + case 0: // ABSOLUTE + $this->correctinfo = $question->correctresponse; + $this->currentinfo = $question->currentresponse; + break; + + case 1: // RELATIVE_NEXT_EXCLUDE_LAST + $this->correctinfo = $question->get_next_answerids($question->correctresponse, false); + $this->currentinfo = $question->get_next_answerids($question->currentresponse, false); + break; + case 2: // RELATIVE_NEXT_INCLUDE_LAST + $this->correctinfo = $question->get_next_answerids($question->correctresponse, true); + $this->currentinfo = $question->get_next_answerids($question->currentresponse, true); + break; + + case 3: // RELATIVE_ONE_PREVIOUS_AND_NEXT + $this->correctinfo = $question->get_previous_and_next_answerids($question->correctresponse, false); + $this->currentinfo = $question->get_previous_and_next_answerids($question->currentresponse, false); + break; + + case 4: // RELATIVE_ALL_PREVIOUS_AND_NEXT + $this->correctinfo = $question->get_previous_and_next_answerids($question->correctresponse, true); + $this->currentinfo = $question->get_previous_and_next_answerids($question->currentresponse, true); + break; + } + } + + protected function get_ordering_item_score($question, $position, $answerid) { + + if (! isset($this->itemscores[$position])) { + + if ($this->correctinfo===null || $this->currentinfo===null) { + $this->get_response_info($question); + } + + $correctinfo = $this->correctinfo; + $currentinfo = $this->currentinfo; + + $score = 0; // actual score for this item + $maxscore = null; // max score for this item + $fraction = 0.0; // $score / $maxscore + $percent = 0; // 100 * $fraction + + switch ($question->options->gradingtype) { + + case 0: // ABSOLUTE + if (isset($correctinfo[$position])) { + if ($correctinfo[$position]==$answerid) { + $score = 1; + } + $maxscore = 1; + } + break; + + case 1; // RELATIVE_NEXT_EXCLUDE_LAST + case 2; // RELATIVE_NEXT_INCLUDE_LAST + if (isset($correctinfo[$answerid])) { + if (isset($currentinfo[$answerid]) && $currentinfo[$answerid]==$correctinfo[$answerid]) { + $score = 1; + } + $maxscore = 1; + } + break; + + case 3; // RELATIVE_ONE_PREVIOUS_AND_NEXT + case 4; // RELATIVE_ALL_PREVIOUS_AND_NEXT + if (isset($correctinfo[$answerid])) { + $maxscore = 0; + $prev = $correctinfo[$answerid]->prev; + $maxscore += count($prev); + $prev = array_intersect($prev, $currentinfo[$answerid]->prev); + $score += count($prev); + $next = $correctinfo[$answerid]->next; + $maxscore += count($next); + $next = array_intersect($next, $currentinfo[$answerid]->next); + $score += count($next); + } + break; + } + + if ($maxscore===null) { + // an unscored item is either an illegal item + // or last item of RELATIVE_NEXT_EXCLUDE_LAST + $class = 'unscored'; + } else { + if ($maxscore==0) { + $fraction = 0.0; + $percent = 0; + } else { + $fraction = ($score / $maxscore); + $percent = round(100 * $fraction, 0); + } + switch (true) { + case ($fraction > 0.999999): $class = 'correct'; break; + case ($fraction < 0.000001): $class = 'incorrect'; break; + case ($fraction >= 0.66): $class = 'partial66'; break; + case ($fraction >= 0.33): $class = 'partial33'; break; + default: $class = 'partial00'; break; + } + $img = $this->feedback_image($fraction); + } + + $score = array($score, $maxscore, $fraction, $percent, $class, $img); + $this->itemscores[$position] = $score; + } + + return $this->itemscores[$position]; + } } diff --git a/question/type/ordering/styles.css b/question/type/ordering/styles.css index 95b0d739c8c..0345d4960b2 100644 --- a/question/type/ordering/styles.css +++ b/question/type/ordering/styles.css @@ -38,29 +38,29 @@ min-height : 18px; } .que.ordering ul.sortablelist li.correct { - background-color : #dff0d8; /* light green */ + background-color : #dff4d8; /* light green */ border-color : #99ff66; /* gentle green */ } .que.ordering ul.sortablelist li.partial66 { - background-color : #dff0d8; /* light green */ + background-color : #dff4d8; /* light green */ border-color : #ff9900; /* dark orange */ } .que.ordering ul.sortablelist li.partial33 { background-color : #ffebcc; /* light orange */ border-color : #ff9900; /* dark orange */ } -.que.ordering ul.sortablelist li.partial01 { - background-color : #f2dede; /* light red */ +.que.ordering ul.sortablelist li.partial00 { + background-color : #ffdddd; /* light red */ border-color : #ff9900; /* dark orange */ } .que.ordering ul.sortablelist li.incorrect { - background-color : #f2dede; /* light red */ + background-color : #ffdddd; /* light red */ border-color : #ff7373; /* gentle red */ } .que.ordering ul.sortablelist li.correct, .que.ordering ul.sortablelist li.partial66, .que.ordering ul.sortablelist li.partial33, -.que.ordering ul.sortablelist li.partial01, +.que.ordering ul.sortablelist li.partial00, .que.ordering ul.sortablelist li.incorrect, .que.ordering ul.sortablelist li.unscored { border-style : solid; diff --git a/question/type/ordering/version.php b/question/type/ordering/version.php index 641ac1155c7..e666df3f586 100644 --- a/question/type/ordering/version.php +++ b/question/type/ordering/version.php @@ -31,5 +31,5 @@ $plugin->cron = 0; $plugin->component = 'qtype_ordering'; $plugin->maturity = MATURITY_STABLE; $plugin->requires = 2010112400; // Moodle 2.0 -$plugin->version = 2015123135; -$plugin->release = '2015-12-31 (35)'; +$plugin->version = 2016010136; +$plugin->release = '2016-01-01 (36)';