MDL-41759 quiz statistics : break down responses for each variant
authorJames Pratt <me@jamiep.org>
Sat, 21 Dec 2013 08:50:19 +0000 (15:50 +0700)
committerJames Pratt <me@jamiep.org>
Fri, 14 Feb 2014 08:23:04 +0000 (15:23 +0700)
13 files changed:
lib/db/install.xml
lib/db/upgrade.php
mod/quiz/report/statistics/lang/en/quiz_statistics.php
mod/quiz/report/statistics/report.php
mod/quiz/report/statistics/statistics_table.php
mod/quiz/report/statistics/tests/fixtures/responsecounts00.csv [new file with mode: 0644]
mod/quiz/report/statistics/tests/stats_from_steps_walkthrough_test.php
question/classes/statistics/responses/analyser.php
question/classes/statistics/responses/analysis_for_actual_response.php
question/classes/statistics/responses/analysis_for_class.php
question/classes/statistics/responses/analysis_for_question.php
question/classes/statistics/responses/analysis_for_subpart.php
version.php

index f266669..7e6c047 100644 (file)
         <FIELD NAME="hashcode" TYPE="char" LENGTH="40" NOTNULL="true" SEQUENCE="false" COMMENT="sha1 hash of serialized qubaids_condition class. Unique for every combination of class name and property."/>
         <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="questionid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="variant" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="subqid" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="aid" TYPE="char" LENGTH="100" NOTNULL="false" SEQUENCE="false"/>
         <FIELD NAME="response" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
index 973d4af..3c60f15 100644 (file)
@@ -2982,7 +2982,13 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2014012400.00);
     }
 
-    if ($oldversion < 2014020500.00) {
+    if ($oldversion < 2014021300.01) {
+        // Delete any cached stats to force recalculation later, then we can be sure that cached records will have the correct
+        // field.
+        $DB->delete_records('question_response_analysis');
+        $DB->delete_records('question_statistics');
+        $DB->delete_records('quiz_statistics');
+
         // Define field variant to be added to question_statistics.
         $table = new xmldb_table('question_statistics');
         $field = new xmldb_field('variant', XMLDB_TYPE_INTEGER, '10', null, null, null, null, 'subquestion');
@@ -2993,7 +2999,22 @@ function xmldb_main_upgrade($oldversion) {
         }
 
         // Main savepoint reached.
-        upgrade_main_savepoint(true, 2014020500.00);
+        upgrade_main_savepoint(true, 2014021300.01);
+    }
+
+    if ($oldversion < 2014021300.02) {
+
+        // Define field variant to be added to question_response_analysis.
+        $table = new xmldb_table('question_response_analysis');
+        $field = new xmldb_field('variant', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, 0, 'questionid');
+
+        // Conditionally launch add field variant.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2014021300.02);
     }
 
     return true;
index 9c9c1c8..b3d57ad 100644 (file)
@@ -39,6 +39,7 @@ $string['completestatsfilename'] = 'completestats';
 $string['count'] = 'Count';
 $string['coursename'] = 'Course name';
 $string['detailedanalysis'] = 'More detailed analysis of the responses to this question';
+$string['detailedanalysisforvariant'] = 'More detailed analysis of the responses to variant {$a} of this question';
 $string['discrimination_index'] = 'Discrimination index';
 $string['discriminative_efficiency'] = 'Discriminative efficiency';
 $string['downloadeverything'] = 'Download full report as {$a->formatsmenu} {$a->downloadbutton}';
@@ -107,3 +108,6 @@ $string['statisticsreport'] = 'Statistics report';
 $string['statisticsreportgraph'] = 'Statistics for question positions';
 $string['statistics:view'] = 'View statistics report';
 $string['statsfor'] = 'Quiz statistics (for {$a})';
+$string['variant'] = 'Variant';
+$string['variantno'] = 'Variant {$a}';
+
index 42ea15d..5a93759 100644 (file)
@@ -72,6 +72,7 @@ class quiz_statistics_report extends quiz_default_report {
         // A qid paramter indicates we should display the detailed analysis of a sub question.
         $qid = optional_param('qid', 0, PARAM_INT);
         $slot = optional_param('slot', 0, PARAM_INT);
+        $variantno = optional_param('variant', null, PARAM_INT);
         $whichattempts = optional_param('whichattempts', $quiz->grademethod, PARAM_INT);
 
         $pageoptions = array();
@@ -170,7 +171,7 @@ class quiz_statistics_report extends quiz_default_report {
                 echo $OUTPUT->notification(get_string('noattempts', 'quiz'));
             }
 
-            foreach($questionstats->any_error_messages() as $errormessage) {
+            foreach ($questionstats->any_error_messages() as $errormessage) {
                 echo $OUTPUT->notification($errormessage);
             }
 
@@ -190,32 +191,36 @@ class quiz_statistics_report extends quiz_default_report {
                     $this->output_statistics_graph($quiz->id, $currentgroup, $whichattempts);
                 }
 
-                foreach ($questions as $slot => $question) {
-                    if (question_bank::get_qtype(
-                            $question->qtype, false)->can_analyse_responses()) {
-                        $this->output_individual_question_response_analysis(
-                                $question, $questionstats->for_slot($slot)->s, $reporturl, $qubaids);
-
-                    } else if ($subqids = $questionstats->for_slot($slot)->get_sub_question_ids()) {
-                        foreach ($subqids as $subqid) {
-                            $this->output_individual_question_response_analysis($questionstats->for_subq($subqid)->question,
-                                                                                $questionstats->for_subq($subqid)->s,
-                                                                                $reporturl,
-                                                                                $qubaids);
-                        }
-                    }
-                }
+                $this->output_all_question_response_analysis($qubaids, $questions, $questionstats, $reporturl);
             }
 
             $this->table->export_class_instance()->finish_document();
 
+        } else if ($qid) {
+            // Report on an individual sub-question indexed questionid.
+            if (is_null($questionstats->for_subq($qid, $variantno))) {
+                print_error('questiondoesnotexist', 'question');
+            }
+
+            $this->output_individual_question_data($quiz, $questionstats->for_subq($qid, $variantno));
+            $this->output_individual_question_response_analysis($questionstats->for_subq($qid, $variantno)->question,
+                                                                $variantno,
+                                                                $questionstats->for_subq($qid, $variantno)->s,
+                                                                $reporturl,
+                                                                $qubaids);
+            // Back to overview link.
+            echo $OUTPUT->box('<a href="' . $reporturl->out() . '">' .
+                              get_string('backtoquizreport', 'quiz_statistics') . '</a>',
+                              'boxaligncenter generalbox boxwidthnormal mdl-align');
         } else if ($slot) {
             // Report on an individual question indexed by position.
             if (!isset($questions[$slot])) {
                 print_error('questiondoesnotexist', 'question');
             }
 
-            if ($questionstats->for_slot($slot)->get_sub_question_ids() || $questionstats->for_slot($slot)->get_variants()) {
+            if ($variantno === null &&
+                                ($questionstats->for_slot($slot)->get_sub_question_ids()
+                                || $questionstats->for_slot($slot)->get_variants())) {
                 if (!$this->table->is_downloading()) {
                     $number = $questionstats->for_slot($slot)->question->number;
                     echo $OUTPUT->heading(get_string('slotstructureanalysis', 'quiz_statistics', $number), 3);
@@ -223,12 +228,12 @@ class quiz_statistics_report extends quiz_default_report {
                 $this->table->define_baseurl(new moodle_url($reporturl, array('slot' => $slot)));
                 $this->table->format_and_add_array_of_rows($questionstats->structure_analysis_for_one_slot($slot));
             } else {
-                $this->output_individual_question_data($quiz, $questionstats->for_slot($slot));
+                $this->output_individual_question_data($quiz, $questionstats->for_slot($slot, $variantno));
                 $this->output_individual_question_response_analysis($questions[$slot],
-                                                                    $questionstats->for_slot($slot)->s,
+                                                                    $variantno,
+                                                                    $questionstats->for_slot($slot, $variantno)->s,
                                                                     $reporturl,
                                                                     $qubaids);
-
             }
             if (!$this->table->is_downloading()) {
                 // Back to overview link.
@@ -239,23 +244,6 @@ class quiz_statistics_report extends quiz_default_report {
                 $this->table->finish_output();
             }
 
-        } else if ($qid) {
-            // Report on an individual sub-question indexed questionid.
-            if (is_null($questionstats->for_subq($qid))) {
-                print_error('questiondoesnotexist', 'question');
-            }
-
-            $this->output_individual_question_data($quiz, $questionstats->for_subq($qid));
-            $this->output_individual_question_response_analysis($questionstats->for_subq($qid)->question,
-                                                                $questionstats->for_subq($qid)->s,
-                                                                $reporturl,
-                                                                $qubaids);
-
-            // Back to overview link.
-            echo $OUTPUT->box('<a href="' . $reporturl->out() . '">' .
-                    get_string('backtoquizreport', 'quiz_statistics') . '</a>',
-                    'boxaligncenter generalbox boxwidthnormal mdl-align');
-
         } else if ($this->table->is_downloading()) {
             // Downloading overview report.
             $quizinfo = $quizstats->get_formatted_quiz_info_data($course, $cm, $quiz);
@@ -305,6 +293,11 @@ class quiz_statistics_report extends quiz_default_report {
         $questioninfotable->data[] = array(get_string('modulename', 'quiz'), $quiz->name);
         $questioninfotable->data[] = array(get_string('questionname', 'quiz_statistics'),
                 $questionstat->question->name.'&nbsp;'.$datumfromtable['actions']);
+
+        if ($questionstat->variant !== null) {
+            $questioninfotable->data[] = array(get_string('variant', 'quiz_statistics'), $questionstat->variant);
+
+        }
         $questioninfotable->data[] = array(get_string('questiontype', 'quiz_statistics'),
                 $datumfromtable['icon'] . '&nbsp;' .
                 question_bank::get_qtype($questionstat->question->qtype, false)->menu_name() . '&nbsp;' .
@@ -364,12 +357,14 @@ class quiz_statistics_report extends quiz_default_report {
 
     /**
      * Display the response analysis for a question.
+     *
      * @param object           $question  the question to report on.
+     * @param int|null         $variantno the variant
      * @param int              $s
      * @param moodle_url       $reporturl the URL to redisplay this report.
      * @param qubaid_condition $qubaids
      */
-    protected function output_individual_question_response_analysis($question, $s, $reporturl, $qubaids) {
+    protected function output_individual_question_response_analysis($question, $variantno, $s, $reporturl, $qubaids) {
         global $OUTPUT;
 
         if (!question_bank::get_qtype($question->qtype, false)->can_analyse_responses()) {
@@ -389,6 +384,9 @@ class quiz_statistics_report extends quiz_default_report {
             if (!empty($question->number)) {
                 $questiontabletitle = '(' . $question->number . ') ' . $questiontabletitle;
             }
+            if (!is_null($variantno)) {
+                $questiontabletitle .= ' '.get_string('variantno', 'quiz_statistics', $variantno);
+            }
             if ($this->table->is_downloading() == 'xhtml') {
                 $questiontabletitle = get_string('analysisofresponsesfor', 'quiz_statistics', $questiontabletitle);
             }
@@ -408,8 +406,13 @@ class quiz_statistics_report extends quiz_default_report {
         if ($this->table->is_downloading()) {
             $exportclass->output_headers($qtable->headers);
         }
-        foreach ($responseanalysis->get_subpart_ids() as $partid) {
-            $subpart = $responseanalysis->get_subpart($partid);
+
+        // Where no variant no is specified the variant no is actually one.
+        if ($variantno === null) {
+            $variantno = 1;
+        }
+        foreach ($responseanalysis->get_subpart_ids($variantno) as $partid) {
+            $subpart = $responseanalysis->get_analysis_for_subpart($variantno, $partid);
             foreach ($subpart->get_response_class_ids() as $responseclassid) {
                 $responseclass = $subpart->get_response_class($responseclassid);
                 $tabledata = $responseclass->data_for_question_response_table($subpart->has_multiple_response_classes(), $partid);
@@ -564,7 +567,7 @@ class quiz_statistics_report extends quiz_default_report {
     protected function get_progress_trace_instance() {
         if ($this->progress === null) {
             if (!$this->table->is_downloading()) {
-                $this->progress =  new \core\progress\display_if_slow(get_string('calculatingallstats', 'quiz_statistics'));
+                $this->progress = new \core\progress\display_if_slow(get_string('calculatingallstats', 'quiz_statistics'));
                 $this->progress->set_display_names();
             } else {
                 $this->progress = new \core\progress\null();
@@ -729,5 +732,57 @@ class quiz_statistics_report extends quiz_default_report {
         }
         return $questions;
     }
-}
 
+    /**
+     * Output all response analysis for all questions, sub-questions and variants. For download in a number of formats.
+     *
+     * @param $qubaids
+     * @param $questions
+     * @param $questionstats
+     * @param $reporturl
+     */
+    protected function output_all_question_response_analysis($qubaids, $questions, $questionstats, $reporturl) {
+        foreach ($questions as $slot => $question) {
+            if (question_bank::get_qtype(
+                $question->qtype, false)->can_analyse_responses()
+            ) {
+                if ($questionstats->for_slot($slot)->get_variants()) {
+                    foreach ($questionstats->for_slot($slot)->get_variants() as $variantno) {
+                        $this->output_individual_question_response_analysis($question,
+                                                                            $variantno,
+                                                                            $questionstats->for_slot($slot, $variantno)->s,
+                                                                            $reporturl,
+                                                                            $qubaids);
+                    }
+                } else {
+                    $this->output_individual_question_response_analysis($question,
+                                                                        null,
+                                                                        $questionstats->for_slot($slot)->s,
+                                                                        $reporturl,
+                                                                        $qubaids);
+                }
+            } else if ($subqids = $questionstats->for_slot($slot)->get_sub_question_ids()) {
+                foreach ($subqids as $subqid) {
+                    if ($variants = $questionstats->for_subq($subqid)->get_variants()) {
+                        foreach ($variants as $variantno) {
+                            $this->output_individual_question_response_analysis(
+                                $questionstats->for_subq($subqid, $variantno)->question,
+                                $variantno,
+                                $questionstats->for_subq($subqid, $variantno)->s,
+                                $reporturl,
+                                $qubaids);
+                        }
+                    } else {
+                        $this->output_individual_question_response_analysis(
+                            $questionstats->for_subq($subqid)->question,
+                            null,
+                            $questionstats->for_subq($subqid)->s,
+                            $reporturl,
+                            $qubaids);
+
+                    }
+                }
+            }
+        }
+    }
+}
index 7c674d0..778ed46 100644 (file)
@@ -203,18 +203,37 @@ class quiz_statistics_table extends flexible_table {
         }
 
         $baseurl = new moodle_url($this->baseurl);
-        if (is_null($questionstat->variant)) {
+        if (!is_null($questionstat->variant)) {
+            if ($questionstat->subquestion) {
+                // Variant of a sub-question.
+                $url = new moodle_url($baseurl, array('qid' => $questionstat->questionid, 'variant' => $questionstat->variant));
+                $name = html_writer::link($url, $name, array('title' => get_string('detailedanalysisforvariant',
+                                                                                   'quiz_statistics',
+                                                                                   $questionstat->variant)));
+            } else if ($questionstat->slot) {
+                // Variant of a question in a slot.
+                $url = new moodle_url($baseurl, array('slot' => $questionstat->slot, 'variant' => $questionstat->variant));
+                $name = html_writer::link($url, $name, array('title' => get_string('detailedanalysisforvariant',
+                                                                                   'quiz_statistics',
+                                                                                   $questionstat->variant)));
+            }
+        } else {
             if ($questionstat->subquestion && !$questionstat->get_variants()) {
+                // Sub question without variants.
                 $url = new moodle_url($baseurl, array('qid' => $questionstat->questionid));
                 $name = html_writer::link($url, $name, array('title' => get_string('detailedanalysis', 'quiz_statistics')));
             } else if ($baseurl->param('slot') === null && $questionstat->slot) {
+                // Question in a slot, we are not on a page showing structural analysis of one slot,
+                // we don't want linking on those pages.
                 $number = $questionstat->question->number;
                 $url = new moodle_url($baseurl, array('slot' => $questionstat->slot));
                 if ($questionstat->get_variants() || $questionstat->get_sub_question_ids()) {
+                    // Question can be broken down into sub-questions or variants. Link will show structural analysis page.
                     $name = html_writer::link($url,
                                               $name,
                                               array('title' => get_string('slotstructureanalysis', 'quiz_statistics', $number)));
                 } else {
+                    // Question cannot be broken down into sub-questions or variants. Link will show response analysis page.
                     $name = html_writer::link($url,
                                               $name,
                                               array('title' => get_string('detailedanalysis', 'quiz_statistics')));
diff --git a/mod/quiz/report/statistics/tests/fixtures/responsecounts00.csv b/mod/quiz/report/statistics/tests/fixtures/responsecounts00.csv
new file mode 100644 (file)
index 0000000..872ca30
--- /dev/null
@@ -0,0 +1,19 @@
+slot,randq,variant,subpart,modelresponse,actualresponse,count
+1,numerical,1,1,"3.142 (3.142..3.142)",3.142,1
+1,numerical,1,1,"3.14 (3.14..3.14)",3.14,7
+1,numerical,1,1,"3.1 (3.1..3.1)",3.1,4
+1,shortanswer,1,1,frog,frog,9
+1,shortanswer,1,1,*,butterfly,2
+1,shortanswer,1,1,toad,toad,2
+2,,1,1,{a} + {b} (±0.01 Relative),9.9,3
+2,,1,1,[NO MATCH],-0.7,1
+2,,1,1,[NO MATCH],-0.2,1
+2,,1,1,[NO MATCH],-1,1
+2,,4,1,{a} + {b} (±0.01 Relative),19.4,2
+2,,4,1,[NO RESPONSE],,1
+2,,4,1,[NO MATCH],-0.4,1
+3,,1,1,frog: amphibian,amphibian,25
+3,,1,2,cat: mammal,mammal,24
+3,,1,2,cat: amphibian,amphibian,1
+3,,1,3,newt: amphibian,amphibian,24
+3,,1,3,newt: mammal,mammal,1
index b9d8306..d0bd87c 100644 (file)
@@ -53,7 +53,7 @@ class quiz_report_statistics_from_steps_testcase extends mod_quiz_attempt_walkth
         return  __DIR__."/fixtures/{$setname}{$test}.csv";
     }
 
-    protected $files = array('questions', 'steps', 'results', 'qstats');
+    protected $files = array('questions', 'steps', 'results', 'qstats', 'responsecounts');
 
     /**
      * Create a quiz add questions to it, walk through quiz attempts and then check results.
@@ -86,7 +86,7 @@ class quiz_report_statistics_from_steps_testcase extends mod_quiz_attempt_walkth
         $qubaids = quiz_statistics_qubaids_condition($this->quiz->id, $groupstudents, $whichattempts);
 
         // We will create some quiz and question stat calculator instances and some response analyser instances, just in order
-        // to check the time of the
+        // to check the last analysed time then returned.
         $quizcalc = new \quiz_statistics\calculator();
         // Should not be a delay of more than one second between the calculation of stats above and here.
         $this->assertTimeCurrent($quizcalc->get_last_calculated_time($qubaids));
@@ -95,14 +95,26 @@ class quiz_report_statistics_from_steps_testcase extends mod_quiz_attempt_walkth
         $this->assertTimeCurrent($qcalc->get_last_calculated_time($qubaids));
 
         foreach ($questions as $question) {
-            if (!question_bank::get_qtype($question->qtype, false)->can_analyse_responses()) {
+            $qtypeobj = question_bank::get_qtype($question->qtype, false);
+            if (!$qtypeobj->can_analyse_responses()) {
                 continue;
             }
             $responesstats = new \core_question\statistics\responses\analyser($question);
             $this->assertTimeCurrent($responesstats->get_last_analysed_time($qubaids));
         }
 
-        // These quiz stats and the question stats found in qstats00.csv were calculated independently in spreadsheet which is
+        for ($rowno = 0; $rowno < $csvdata['responsecounts']->getRowCount(); $rowno++) {
+            $responsecount = $csvdata['responsecounts']->getRow($rowno);
+            if ($responsecount['randq'] == '') {
+                $question = $questions[$responsecount['slot']];
+            } else {
+                $qid = $this->randqids[$responsecount['slot']][$responsecount['randq']];
+                $question = question_finder::get_instance()->load_question_data($qid);
+            }
+            $this->assert_response_count_equals($question, $qubaids, $responsecount);
+        }
+
+            // These quiz stats and the question stats found in qstats00.csv were calculated independently in spreadsheet which is
         // available in open document or excel format here :
         // https://github.com/jamiepratt/moodle-quiz-tools/tree/master/statsspreadsheet
         $quizstatsexpected = array(
@@ -146,8 +158,6 @@ class quiz_report_statistics_from_steps_testcase extends mod_quiz_attempt_walkth
             $this->assert_stat_equals($questionstats, 1, null, 'numerical', $statname, $expected);
         }
 
-
-
         // These variant's stats are calculated in stats_for_variant_1.xls and stats_for_variant_8.xls
         // The calculations in the spreadsheets are the same but applied just to the attempts where the variants appeared.
 
@@ -172,10 +182,64 @@ class quiz_report_statistics_from_steps_testcase extends mod_quiz_attempt_walkth
                                                     'slot' => 2,
                                                     'subquestion' => false));
         foreach ($statsforslot2variants as $variant => $stats) {
-             foreach ($stats as $statname => $expected) {
-                 $this->assert_stat_equals($questionstats, 2, $variant, null, $statname, $expected);
-             }
+            foreach ($stats as $statname => $expected) {
+                $this->assert_stat_equals($questionstats, 2, $variant, null, $statname, $expected);
+            }
+        }
+    }
+
+    protected function assert_response_count_equals($question, $qubaids, $responsecount) {
+        $responesstats = new \core_question\statistics\responses\analyser($question);
+        $analysis = $responesstats->load_cached($qubaids);
+        if (!isset($responsecount['subpart'])) {
+            $subpart = 1;
+        } else {
+            $subpart = $responsecount['subpart'];
+        }
+        list($subpartid, $responseclassid) = $this->get_response_subpart_and_class_id($question,
+                                                                                      $subpart,
+                                                                                      $responsecount['modelresponse']);
+
+        $subpartanalysis = $analysis->get_analysis_for_subpart($responsecount['variant'], $subpartid);
+        $responseclassanalysis = $subpartanalysis->get_response_class($responseclassid);
+        $actualresponsecounts = $responseclassanalysis->data_for_question_response_table('', '');
+        if ($responsecount['modelresponse'] !== '[NO RESPONSE]') {
+            foreach ($actualresponsecounts as $actualresponsecount) {
+                if ($actualresponsecount->response == $responsecount['actualresponse']) {
+                    $this->assertEquals($responsecount['count'], $actualresponsecount->count);
+                    return;
+                }
+            }
+            throw new coding_exception("Actual response '{$responsecount['actualresponse']}' not found.");
+        } else {
+            $actualresponsecount = array_pop($actualresponsecounts);
+            $this->assertEquals($responsecount['count'], $actualresponsecount->count);
+        }
+    }
+
+    protected function get_response_subpart_and_class_id($question, $subpart, $modelresponse) {
+        $qtypeobj = question_bank::get_qtype($question->qtype, false);
+        $possibleresponses = $qtypeobj->get_possible_responses($question);
+        $possibleresponsesubpartids = array_keys($possibleresponses);
+        if (!isset($possibleresponsesubpartids[$subpart - 1])) {
+            throw new coding_exception("Subpart '{$subpart}' not found.");
+        }
+        $subpartid = $possibleresponsesubpartids[$subpart - 1];
+
+        if ($modelresponse == '[NO RESPONSE]') {
+            return array($subpartid, null);
+
+        } else if ($modelresponse == '[NO MATCH]') {
+            return array($subpartid, 0);
+        }
+
+        $modelresponses = array();
+        foreach ($possibleresponses[$subpartid] as $responseclassid => $subpartpossibleresponse) {
+            $modelresponses[$responseclassid] = $subpartpossibleresponse->responseclass;
         }
+        $this->assertContains($modelresponse, $modelresponses);
+        $responseclassid = array_search($modelresponse, $modelresponses);
+        return array($subpartid, $responseclassid);
     }
 
     /**
index bfaa26f..191cf93 100644 (file)
@@ -45,8 +45,10 @@ class analyser {
     public $analysis;
 
     /**
-     * @var array Two index array first index is unique for each sub question part, the second index is the 'class' that this sub
-     *          question part can be classified into. This is the return value from {@link \question_type::get_possible_responses()}
+     * @var array Two index array first index is unique string for each sub question part, the second string index is the 'class'
+     * that sub-question part can be classified into.
+     *
+     * This is the return value from {@link \question_type::get_possible_responses()} see that method for fuller documentation.
      */
     public $responseclasses = array();
 
@@ -117,7 +119,7 @@ class analyser {
         // Analyse it.
         foreach ($questionattempts as $qa) {
             $responseparts = $qa->classify_response();
-            $this->analysis->count_response_parts($responseparts);
+            $this->analysis->count_response_parts($qa->get_variant(), $responseparts);
         }
         $this->analysis->cache($qubaids, $this->questiondata->id);
         return $this->analysis;
@@ -144,7 +146,7 @@ class analyser {
         }
 
         foreach ($rows as $row) {
-            $class = $this->analysis->get_subpart($row->subqid)->get_response_class($row->aid);
+            $class = $this->analysis->get_analysis_for_subpart($row->variant, $row->subqid)->get_response_class($row->aid);
             $class->add_response_and_count($row->response, $row->credit, $row->rcount);
         }
         return $this->analysis;
index d4d8f8e..a491777 100644 (file)
@@ -58,18 +58,19 @@ class analysis_for_actual_response {
         $this->count++;
     }
 
-
     /**
      * @param \qubaid_condition $qubaids
      * @param int               $questionid the question id
+     * @param int               $variantno
      * @param string            $subpartid
      * @param string            $responseclassid
      */
-    public function cache($qubaids, $questionid, $subpartid, $responseclassid) {
+    public function cache($qubaids, $questionid, $variantno, $subpartid, $responseclassid) {
         global $DB;
         $row = new \stdClass();
         $row->hashcode = $qubaids->get_hash_code();
         $row->questionid = $questionid;
+        $row->variant = $variantno;
         $row->subqid = $subpartid;
         if ($responseclassid === '') {
             $row->aid = null;
index 9c83fa1..8f43ee7 100644 (file)
@@ -85,11 +85,12 @@ class analysis_for_class {
     /**
      * @param \qubaid_condition $qubaids
      * @param int               $questionid the question id
+     * @param int               $variantno
      * @param string            $subpartid
      */
-    public function cache($qubaids, $questionid, $subpartid) {
+    public function cache($qubaids, $questionid, $variantno, $subpartid) {
         foreach ($this->actualresponses as $response => $actualresponse) {
-            $actualresponse->cache($qubaids, $questionid, $subpartid, $this->responseclassid, $response);
+            $actualresponse->cache($qubaids, $questionid, $variantno, $subpartid, $this->responseclassid, $response);
         }
     }
 
index ed0ef93..0fd1de6 100644 (file)
@@ -44,39 +44,79 @@ defined('MOODLE_INTERNAL') || die();
 class analysis_for_question {
 
     /**
-     * Takes either a two index array as a parameter with keys subpartid and classid and values possible_response.
-     * Or takes an array of {@link responses_for_classes} objects.
+     * Constructor method.
      *
-     * @param $subparts[string]array[]\question_possible_response $array
+     * @param array[] Two index array, first index is unique string for each sub question part,
+     *                    the second string index is the 'class' that sub-question part can be classified into.
+     *                    Value in array is instance of {@link \question_possible_response}
+     *                    This is the return value from {@link \question_type::get_possible_responses()}
+     *                    see that method for fuller documentation.
      */
-    public function __construct(array $subparts = null) {
-        if (!is_null($subparts)) {
-            foreach ($subparts as $subpartid => $classes) {
-                $this->subparts[$subpartid] = new analysis_for_subpart($classes);
-            }
+    public function __construct(array $possiblereponses = null) {
+        if ($possiblereponses !== null) {
+            $this->possibleresponses = $possiblereponses;
+        }
+    }
+
+    /**
+     * @var array[] See description above in constructor method.
+     */
+    protected $possibleresponses = array();
+
+    /**
+     * A multidimensional array whose first index is variant no and second index is subpart id, array contents are of type
+     * {@link analysis_for_subpart}.
+     *
+     * @var array[]
+     */
+    protected $subparts = array();
+
+    /**
+     * Initialise data structure for response analysis of one variant.
+     *
+     * @param int $variantno
+     */
+    protected function initialise_stats_for_variant($variantno) {
+        $this->subparts[$variantno] = array();
+        foreach ($this->possibleresponses as $subpartid => $classes) {
+            $this->subparts[$variantno][$subpartid] = new analysis_for_subpart($classes);
         }
     }
 
     /**
-     * @var analysis_for_subpart[]
+     * Variant nos found in this question's attempt data.
+     *
+     * @return int[]
      */
-    protected $subparts;
+    public function get_variant_nos() {
+        return array_keys($this->subparts);
+    }
 
     /**
      * Unique ids for sub parts.
      *
+     * @param int $variantno
      * @return string[]
      */
-    public function get_subpart_ids() {
-        return array_keys($this->subparts);
+    public function get_subpart_ids($variantno) {
+        return array_keys($this->subparts[$variantno]);
     }
 
     /**
+     * Get the response counts etc. for variant $variantno, question sub part $subpartid.
+     *
+     * Or if there is no recorded analysis yet then initialise the data structure for that part of the analysis and return the
+     * initialised analysis objects.
+     *
+     * @param int    $variantno
      * @param string $subpartid id for sub part.
      * @return analysis_for_subpart
      */
-    public function get_subpart($subpartid) {
-        return $this->subparts[$subpartid];
+    public function get_analysis_for_subpart($variantno, $subpartid) {
+        if (!isset($this->subparts[$variantno])) {
+            $this->initialise_stats_for_variant($variantno);
+        }
+        return $this->subparts[$variantno][$subpartid];
     }
 
     /**
@@ -85,9 +125,11 @@ class analysis_for_question {
      * @return bool whether this question has (a subpart with) more than one response class.
      */
     public function has_multiple_response_classes() {
-        foreach ($this->subparts as $subpart) {
-            if ($subpart->has_multiple_response_classes()) {
-                return true;
+        foreach ($this->get_variant_nos() as $variantno) {
+            foreach ($this->get_subpart_ids($variantno) as $subpartid) {
+                if ($this->get_analysis_for_subpart($variantno, $subpartid)->has_multiple_response_classes()) {
+                    return true;
+                }
             }
         }
         return false;
@@ -99,17 +141,23 @@ class analysis_for_question {
      * @return bool whether this analysis has more than one subpart.
      */
     public function has_subparts() {
-        return count($this->subparts) > 1;
+        foreach ($this->get_variant_nos() as $variantno) {
+            if (count($this->get_subpart_ids($variantno)) > 1) {
+                return true;
+            }
+        }
+        return false;
     }
 
     /**
      * Takes an array of {@link \question_classified_response} and adds counts of the responses to the sub parts and classes.
      *
-     * @var \question_classified_response[] $responseparts keys are sub-part id.
+     * @param int                             $variantno
+     * @param \question_classified_response[] $responseparts keys are sub-part id.
      */
-    public function count_response_parts($responseparts) {
+    public function count_response_parts($variantno, $responseparts) {
         foreach ($responseparts as $subpartid => $responsepart) {
-            $this->get_subpart($subpartid)->count_response($responsepart);
+            $this->get_analysis_for_subpart($variantno, $subpartid)->count_response($responsepart);
         }
     }
 
@@ -118,8 +166,10 @@ class analysis_for_question {
      * @param int               $questionid the question id
      */
     public function cache($qubaids, $questionid) {
-        foreach ($this->subparts as $subpartid => $subpart) {
-            $subpart->cache($qubaids, $questionid, $subpartid);
+        foreach ($this->get_variant_nos() as $variantno) {
+            foreach ($this->get_subpart_ids($variantno) as $subpartid) {
+                $this->get_analysis_for_subpart($variantno, $subpartid)->cache($qubaids, $questionid, $variantno, $subpartid);
+            }
         }
     }
 
@@ -129,9 +179,11 @@ class analysis_for_question {
      *      the model response.
      */
     public function has_actual_responses() {
-        foreach ($this->subparts as $subpartid => $subpart) {
-            if ($subpart->has_actual_responses()) {
-                return true;
+        foreach ($this->get_variant_nos() as $variantno) {
+            foreach ($this->get_subpart_ids($variantno) as $subpartid) {
+                if ($this->get_analysis_for_subpart($variantno, $subpartid)->has_actual_responses()) {
+                    return true;
+                }
             }
         }
         return false;
index 12ca000..53d9913 100644 (file)
@@ -80,11 +80,12 @@ class analysis_for_subpart {
     /**
      * @param \qubaid_condition $qubaids
      * @param int               $questionid the question id
+     * @param int               $variantno
      * @param string            $subpartid
      */
-    public function cache($qubaids, $questionid, $subpartid) {
+    public function cache($qubaids, $questionid, $variantno, $subpartid) {
         foreach ($this->responseclasses as $responseclassid => $responseclass) {
-            $responseclass->cache($qubaids, $questionid, $subpartid, $responseclassid);
+            $responseclass->cache($qubaids, $questionid, $variantno, $subpartid, $responseclassid);
         }
     }
 
index fdd4e15..09067e7 100644 (file)
@@ -29,7 +29,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2014021300.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2014021300.02;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.