MDL-41751 changes to api of question_response_analyser
authorJamie Pratt <me@jamiep.org>
Fri, 27 Sep 2013 06:50:12 +0000 (13:50 +0700)
committerJamie Pratt <me@jamiep.org>
Fri, 27 Sep 2013 10:38:13 +0000 (17:38 +0700)
and code refactoring and clean up.

mod/quiz/report/statistics/report.php
mod/quiz/report/statistics/statistics_question_table.php
question/behaviour/behaviourbase.php
question/classes/statistics/responses/analyser.php
question/classes/statistics/responses/analysis_for_actual_response.php [new file with mode: 0644]
question/classes/statistics/responses/analysis_for_class.php [new file with mode: 0644]
question/classes/statistics/responses/analysis_for_question.php [new file with mode: 0644]
question/classes/statistics/responses/analysis_for_subpart.php [new file with mode: 0644]
question/engine/questionattempt.php

index 5179ee8..19fa679 100644 (file)
@@ -58,7 +58,7 @@ class quiz_statistics_report extends quiz_default_report {
         $download = optional_param('download', '', PARAM_ALPHA);
         $everything = optional_param('everything', 0, PARAM_BOOL);
         $recalculate = optional_param('recalculate', 0, PARAM_BOOL);
-        // A qid paramter indicates we should display the detailed analysis of a question.
+        // 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);
 
@@ -135,7 +135,6 @@ class quiz_statistics_report extends quiz_default_report {
             $questionstats = array();
             $subquestionstats = array();
         }
-        $quizinfo = $quizstats->get_formatted_quiz_info_data($course, $cm, $quiz);
 
         // Set up the table, if there is data.
         if ($quizstats->s()) {
@@ -166,6 +165,7 @@ class quiz_statistics_report extends quiz_default_report {
 
         if ($everything) { // Implies is downloading.
             // Overall report, then the analysis of each question.
+            $quizinfo = $quizstats->get_formatted_quiz_info_data($course, $cm, $quiz);
             $this->download_quiz_info_table($quizinfo);
 
             if ($quizstats->s()) {
@@ -209,7 +209,7 @@ class quiz_statistics_report extends quiz_default_report {
 
         } else if ($qid) {
             // Report on an individual sub-question indexed questionid.
-            if (!isset($subquestions[$qid])) {
+            if (!isset($subquestionstats[$qid])) {
                 print_error('questiondoesnotexist', 'question');
             }
 
@@ -224,6 +224,7 @@ class quiz_statistics_report extends quiz_default_report {
 
         } else if ($this->table->is_downloading()) {
             // Downloading overview report.
+            $quizinfo = $quizstats->get_formatted_quiz_info_data($course, $cm, $quiz);
             $this->download_quiz_info_table($quizinfo);
             $this->output_quiz_structure_analysis_table($quizstats->s(), $questionstats, $subquestionstats);
             $this->table->finish_output();
@@ -234,6 +235,7 @@ class quiz_statistics_report extends quiz_default_report {
             echo $this->output_caching_info($quizstats, $quiz->id, $currentgroup,
                     $groupstudents, $useallattempts, $reporturl);
             echo $this->everything_download_options();
+            $quizinfo = $quizstats->get_formatted_quiz_info_data($course, $cm, $quiz);
             echo $this->output_quiz_info_table($quizinfo);
             if ($quizstats->s()) {
                 echo $OUTPUT->heading(get_string('quizstructureanalysis', 'quiz_statistics'));
@@ -248,10 +250,8 @@ class quiz_statistics_report extends quiz_default_report {
     /**
      * Display the statistical and introductory information about a question.
      * Only called when not downloading.
-     * @param object $quiz the quiz settings.
+     * @param object                                         $quiz         the quiz settings.
      * @param \core_question\statistics\questions\calculated $questionstat the question to report on.
-     * @param moodle_url $reporturl the URL to resisplay this report.
-     * @param object $quizstats Holds the quiz statistics.
      */
     protected function output_individual_question_data($quiz, $questionstat) {
         global $OUTPUT;
@@ -329,8 +329,9 @@ class quiz_statistics_report extends quiz_default_report {
 
     /**
      * Display the response analysis for a question.
-     * @param object     $question  the question to report on.
-     * @param moodle_url $reporturl the URL to resisplay this report.
+     * @param object           $question  the question to report on.
+     * @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) {
@@ -354,8 +355,7 @@ class quiz_statistics_report extends quiz_default_report {
                 $questiontabletitle = '(' . $question->number . ') ' . $questiontabletitle;
             }
             if ($this->table->is_downloading() == 'xhtml') {
-                $questiontabletitle = get_string('analysisofresponsesfor',
-                        'quiz_statistics', $questiontabletitle);
+                $questiontabletitle = get_string('analysisofresponsesfor', 'quiz_statistics', $questiontabletitle);
             }
 
             // Set up the table.
@@ -366,38 +366,20 @@ class quiz_statistics_report extends quiz_default_report {
             }
         }
 
-        $responesstats = new \core_question\statistics\responses\analyser($question);
-        $responesstats->load_cached($qubaids);
+        $responesanalyser = new \core_question\statistics\responses\analyser($question);
+        $responseanalysis = $responesanalyser->load_cached($qubaids);
 
-        $qtable->question_setup($reporturl, $question, $s, $responesstats);
+        $qtable->question_setup($reporturl, $question, $s, $responseanalysis);
         if ($this->table->is_downloading()) {
             $exportclass->output_headers($qtable->headers);
         }
-
-        foreach ($responesstats->responseclasses as $partid => $partclasses) {
-            $rowdata = new stdClass();
-            $rowdata->part = $partid;
-            foreach ($partclasses as $responseclassid => $responseclass) {
-                $rowdata->responseclass = $responseclass->responseclass;
-
-                $responsesdata = $responesstats->responses[$partid][$responseclassid];
-                if (empty($responsesdata)) {
-                    if (!array_key_exists('responseclass', $qtable->columns)) {
-                        $rowdata->response = $responseclass->responseclass;
-                    } else {
-                        $rowdata->response = '';
-                    }
-                    $rowdata->fraction = $responseclass->fraction;
-                    $rowdata->count = 0;
-                    $qtable->add_data_keyed($qtable->format_row($rowdata));
-                    continue;
-                }
-
-                foreach ($responsesdata as $response => $data) {
-                    $rowdata->response = $response;
-                    $rowdata->fraction = $data->fraction;
-                    $rowdata->count = $data->count;
-                    $qtable->add_data_keyed($qtable->format_row($rowdata));
+        foreach ($responseanalysis->get_subpart_ids() as $partid) {
+            $subpart = $responseanalysis->get_subpart($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);
+                foreach ($tabledata as $row) {
+                    $qtable->add_data_keyed($qtable->format_row($row));
                 }
             }
         }
@@ -531,7 +513,7 @@ class quiz_statistics_report extends quiz_default_report {
                                                $groupstudents, count($questions), $qcalc->get_sum_of_mark_variance());
 
             if ($quizstats->s()) {
-                $this->calculate_responses_for_all_questions_and_subquestions($qubaids, $questions, $subquestionstats);
+                $this->analyse_responses_for_all_questions_and_subquestions($qubaids, $questions, $subquestionstats);
             }
         } else {
             $quizstats = $quizcalc->get_cached($qubaids);
@@ -541,7 +523,7 @@ class quiz_statistics_report extends quiz_default_report {
         return array($quizstats, $questionstats, $subquestionstats);
     }
 
-    protected function calculate_responses_for_all_questions_and_subquestions($qubaids, $questions, $subquestionstats) {
+    protected function analyse_responses_for_all_questions_and_subquestions($qubaids, $questions, $subquestionstats) {
 
         $done = array();
         foreach ($questions as $question) {
index 4bfc411..5591e50 100644 (file)
@@ -38,9 +38,12 @@ require_once($CFG->libdir . '/tablelib.php');
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class quiz_statistics_question_table extends flexible_table {
-    /** @var object this question. */
+    /** @var object full question object for this question. */
     protected $questiondata;
 
+    /** @var  int no of attempts. */
+    protected $s;
+
     /**
      * Constructor.
      * @param int $qid the id of the particular question whose statistics are being
@@ -51,12 +54,12 @@ class quiz_statistics_question_table extends flexible_table {
     }
 
     /**
-     * @param moodle_url                                   $reporturl
-     * @param object                                       $questiondata
-     * @param integer                                      $s               number of attempts on this question.
-     * @param \core_question\statistics\responses\analyser $responesstats
+     * @param moodle_url $reporturl
+     * @param object     $questiondata
+     * @param integer    $s             number of attempts on this question.
+     * @param \core_question\statistics\responses\analysis_for_question $responseanalysis
      */
-    public function question_setup($reporturl, $questiondata, $s, \core_question\statistics\responses\analyser $responesstats) {
+    public function question_setup($reporturl, $questiondata, $s, $responseanalysis) {
         $this->questiondata = $questiondata;
         $this->s = $s;
 
@@ -68,16 +71,16 @@ class quiz_statistics_question_table extends flexible_table {
         $columns = array();
         $headers = array();
 
-        if ($responesstats->has_subparts()) {
+        if ($responseanalysis->has_subparts()) {
             $columns[] = 'part';
             $headers[] = get_string('partofquestion', 'quiz_statistics');
         }
 
-        if ($responesstats->has_response_classes()) {
+        if ($responseanalysis->has_multiple_response_classes()) {
             $columns[] = 'responseclass';
             $headers[] = get_string('modelresponse', 'quiz_statistics');
 
-            if ($responesstats->has_actual_responses()) {
+            if ($responseanalysis->has_actual_responses()) {
                 $columns[] = 'response';
                 $headers[] = get_string('actualresponse', 'quiz_statistics');
             }
@@ -129,7 +132,7 @@ class quiz_statistics_question_table extends flexible_table {
 
     /**
      * The frequency with which this response was given.
-     * @param object $response containst the data to display.
+     * @param object $response contains the data to display.
      * @return string contents of this table cell.
      */
     protected function col_frequency($response) {
index a538452..d8f4b2a 100644 (file)
@@ -301,12 +301,7 @@ abstract class question_behaviour {
     }
 
     /**
-     * @return array subpartid => object with fields
-     *      ->responseclassid matches one of the values returned from
-     *                        quetion_type::get_possible_responses.
-     *      ->response the actual response the student gave to this part, as a string.
-     *      ->fraction the credit awarded for this subpart, may be null.
-     *      returns an empty array if no analysis is possible.
+     * @return question_possible_response[] where keys are subpartid or an empty array if no classification is possible.
      */
     public function classify_response() {
         return $this->question->classify_response($this->qa->get_last_qt_data());
index ae3d206..0759c85 100644 (file)
@@ -37,41 +37,31 @@ defined('MOODLE_INTERNAL') || die();
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class analyser {
-    /** @var object the data from the database that defines the question. */
+    /** @var object full question data from db. */
     protected $questiondata;
 
     /**
-     * @var array This is a multi-dimensional array that stores the results of
-     * the analysis.
-     *
-     * The description of {@link question_type::get_possible_responses()} should
-     * help understand this description.
-     *
-     * $this->responses[$subpartid][$responseclassid][$response] is an
-     * object with two fields, ->count and ->fraction.
+     * @var analysis_for_question
      */
-    public $responses = array();
+    public $analysis;
 
     /**
-     * @var array $this->fractions[$subpartid][$responseclassid] is an object
-     * with two fields, ->responseclass and ->fraction.
+     * @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()}
      */
     public $responseclasses = array();
 
     /**
      * Create a new instance of this class for holding/computing the statistics
      * for a particular question.
-     * @param object $questiondata the data from the database defining this question.
+     *
+     * @param object $questiondata the full question data from the database defining this question.
      */
     public function __construct($questiondata) {
         $this->questiondata = $questiondata;
+        $qtypeobj = \question_bank::get_qtype($this->questiondata->qtype);
+        $this->analysis = new analysis_for_question($qtypeobj->get_possible_responses($this->questiondata));
 
-        $this->responseclasses = \question_bank::get_qtype($questiondata->qtype)->get_possible_responses($questiondata);
-        foreach ($this->responseclasses as $subpartid => $responseclasses) {
-            foreach ($responseclasses as $responseclassid => $notused) {
-                $this->responses[$subpartid][$responseclassid] = array();
-            }
-        }
     }
 
     /**
@@ -82,8 +72,7 @@ class analyser {
     }
 
     /**
-     * @return bool whether this analysis has (a subpart with) more than one
-     *      response class.
+     * @return bool whether this analysis has (a subpart with) more than one response class.
      */
     public function has_response_classes() {
         foreach ($this->responseclasses as $partclasses) {
@@ -119,6 +108,7 @@ class analyser {
      * Analyse all the response data for for all the specified attempts at
      * this question.
      * @param \qubaid_condition $qubaids which attempts to consider.
+     * @return analysis_for_question
      */
     public function calculate($qubaids) {
         // Load data.
@@ -127,91 +117,53 @@ class analyser {
 
         // Analyse it.
         foreach ($questionattempts as $qa) {
-            $this->add_data_from_one_attempt($qa);
+            $responseparts = $qa->classify_response();
+            $this->analysis->count_response_parts($responseparts);
         }
-        $this->store_cached($qubaids);
-
+        $this->analysis->cache($qubaids, $this->questiondata->id);
+        return $this->analysis;
     }
 
-    /**
-     * Analyse the data from one question attempt.
-     * @param \question_attempt $qa the data to analyse.
-     */
-    protected function add_data_from_one_attempt(\question_attempt $qa) {
-        $blankresponse = \question_classified_response::no_response();
-
-        $partresponses = $qa->classify_response();
-        foreach ($partresponses as $subpartid => $partresponse) {
-            if (!isset($this->responses[$subpartid][$partresponse->responseclassid]
-                    [$partresponse->response])) {
-                $resp = new \stdClass();
-                $resp->count = 0;
-                if (!is_null($partresponse->fraction)) {
-                    $resp->fraction = $partresponse->fraction;
-                } else {
-                    $resp->fraction = $this->responseclasses[$subpartid]
-                            [$partresponse->responseclassid]->fraction;
-                }
+    /** @var integer Time after which responses are automatically reanalysed. */
+    const TIME_TO_CACHE = 900; // 15 minutes.
 
-                $this->responses[$subpartid][$partresponse->responseclassid]
-                        [$partresponse->response] = $resp;
-            }
-
-            $this->responses[$subpartid][$partresponse->responseclassid]
-                    [$partresponse->response]->count += 1;
-        }
-    }
 
     /**
-     * Store the computed response analysis in the question_response_analysis table.
-     * @param \qubaid_condition $qubaids
-     * data corresponding to.
-     * @return bool true if cached data was found in the database and loaded, otherwise false, to mean no data was loaded.
+     * Retrieve the computed response analysis from the question_response_analysis table.
+     *
+     * @param \qubaid_condition $qubaids which attempts to get cached response analysis for.
+     * @return analysis_for_question|boolean analysis or false if no cached analysis found.
      */
     public function load_cached($qubaids) {
         global $DB;
 
-        $rows = $DB->get_records('question_response_analysis',
-                array('hashcode' => $qubaids->get_hash_code(), 'questionid' => $this->questiondata->id));
+        $timemodified = time() - self::TIME_TO_CACHE;
+        $rows = $DB->get_records_select('question_response_analysis', 'hashcode = ? AND questionid = ? AND timemodified > ? ',
+                                        array($qubaids->get_hash_code(), $this->questiondata->id, $timemodified));
         if (!$rows) {
             return false;
         }
 
         foreach ($rows as $row) {
-            $this->responses[$row->subqid][$row->aid][$row->response] = new \stdClass();
-            $this->responses[$row->subqid][$row->aid][$row->response]->count = $row->rcount;
-            $this->responses[$row->subqid][$row->aid][$row->response]->fraction = $row->credit;
+            $class = $this->analysis->get_subpart($row->subqid)->get_response_class($row->aid);
+            $class->add_response_and_count($row->response, $row->credit, $row->rcount);
         }
-        return true;
+        return $this->analysis;
     }
 
+
     /**
-     * Store the computed response analysis in the question_response_analysis table.
-     * @param \qubaid_condition $qubaids
+     * Find time of non-expired analysis in the database.
+     *
+     * @param $qubaids \qubaid_condition
+     * @return integer|boolean Time of cached record that matches this qubaid_condition or false if none found.
      */
-    public function store_cached($qubaids) {
+    public function get_last_analysed_time($qubaids) {
         global $DB;
 
-        $cachetime = time();
-        foreach ($this->responses as $subpartid => $partdata) {
-            foreach ($partdata as $responseclassid => $classdata) {
-                foreach ($classdata as $response => $data) {
-                    $row = new \stdClass();
-                    $row->hashcode = $qubaids->get_hash_code();
-                    $row->questionid = $this->questiondata->id;
-                    $row->subqid = $subpartid;
-                    if ($responseclassid === '') {
-                        $row->aid = null;
-                    } else {
-                        $row->aid = $responseclassid;
-                    }
-                    $row->response = $response;
-                    $row->rcount = $data->count;
-                    $row->credit = $data->fraction;
-                    $row->timemodified = $cachetime;
-                    $DB->insert_record('question_response_analysis', $row, false);
-                }
-            }
-        }
+        $timemodified = time() - self::TIME_TO_CACHE;
+        return $DB->get_field_select('question_response_analysis', 'timemodified', 'hashcode = ? AND timemodified > ? '.
+                                                          'ORDER BY timemodified DESC LIMIT 1',
+                                     array($qubaids->get_hash_code(), $timemodified));
     }
 }
diff --git a/question/classes/statistics/responses/analysis_for_actual_response.php b/question/classes/statistics/responses/analysis_for_actual_response.php
new file mode 100644 (file)
index 0000000..d1ff857
--- /dev/null
@@ -0,0 +1,106 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * ${filedescription}
+ *
+ * @package    ${package}_{subpackage}
+ * @copyright  2013 The Open University
+ * @author     James Pratt me@jamiep.org
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_question\statistics\responses;
+
+
+class analysis_for_actual_response {
+    /**
+     * @var int count of this response
+     */
+    protected $count;
+
+    /**
+     * @var float grade for this response, normally between 0 and 1.
+     */
+    protected $fraction;
+
+    /**
+     * @var string the response as it will be displayed in report.
+     */
+    protected $response;
+
+    /**
+     * @param string $response
+     * @param float  $fraction
+     * @param int    $count     defaults to zero, this param used when loading from db.
+     */
+    public function __construct($response, $fraction, $count = 0) {
+        $this->response = $response;
+        $this->fraction = $fraction;
+        $this->count = $count;
+    }
+
+    /**
+     * Used to count the occurrences of response sub parts.
+     */
+    public function increment_count() {
+        $this->count++;
+    }
+
+
+    /**
+     * @param \qubaid_condition $qubaids
+     * @param int               $questionid the question id
+     * @param string            $subpartid
+     * @param string            $responseclassid
+     */
+    public function cache($qubaids, $questionid, $subpartid, $responseclassid) {
+        global $DB;
+        $row = new \stdClass();
+        $row->hashcode = $qubaids->get_hash_code();
+        $row->questionid = $questionid;
+        $row->subqid = $subpartid;
+        if ($responseclassid === '') {
+            $row->aid = null;
+        } else {
+            $row->aid = $responseclassid;
+        }
+        $row->response = $this->response;
+        $row->rcount = $this->count;
+        $row->credit = $this->fraction;
+        $row->timemodified = time();
+        $DB->insert_record('question_response_analysis', $row, false);
+    }
+
+    public function response_matches($response) {
+        return $response == $this->response;
+    }
+
+    /**
+     * @param string $partid
+     * @param string $modelresponse
+     * @return object
+     */
+    public function data_for_question_response_table($partid, $modelresponse) {
+        $rowdata = new \stdClass();
+        $rowdata->part = $partid;
+        $rowdata->responseclass = $modelresponse;
+        $rowdata->response = $this->response;
+        $rowdata->fraction = $this->fraction;
+        $rowdata->count = $this->count;
+        return $rowdata;
+    }
+}
diff --git a/question/classes/statistics/responses/analysis_for_class.php b/question/classes/statistics/responses/analysis_for_class.php
new file mode 100644 (file)
index 0000000..471f6e9
--- /dev/null
@@ -0,0 +1,141 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * ${filedescription}
+ *
+ * @package    ${package}_{subpackage}
+ * @copyright  2013 The Open University
+ * @author     James Pratt me@jamiep.org
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_question\statistics\responses;
+
+
+
+/**
+ * Represents an actual part of the response that has been classified in a class of responses for this sub part of the question.
+ *
+ * A question and it's response is represented as having one or more sub parts where the response to each sub-part might fall
+ * into one of one or more classes.
+ *
+ * No response is one possible class of response to a question.
+ *
+ * @copyright  2010 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class analysis_for_class {
+
+    /**
+     * @var string
+     */
+    protected $responseclassid;
+
+    /**
+     * @var string
+     */
+    protected $modelresponse;
+
+    /** @var string the (partial) credit awarded for this responses. */
+    protected $fraction;
+
+    /**
+     *
+     * @var analysis_for_actual_response[] key is the actual response represented as a string as it will be displayed in report.
+     */
+    protected $actualresponses = array();
+
+    /**
+     * Constructor, just an easy way to set the fields.
+     * @param \question_possible_response $possibleresponse
+     * @param string                      $responseclassid
+     */
+    public function __construct($possibleresponse, $responseclassid) {
+        $this->modelresponse = $possibleresponse->responseclass;
+        $this->fraction = $possibleresponse->fraction;
+        $this->responseclassid = $responseclassid;
+    }
+
+    /**
+     * @param string $actualresponse
+     * @param float|null $fraction
+     */
+    public function count_response($actualresponse, $fraction) {
+        if (!isset($this->actualresponses[$actualresponse])) {
+            if ($fraction === null) {
+                $fraction = $this->fraction;
+            }
+            $this->actualresponses[$actualresponse] = new analysis_for_actual_response($actualresponse, $fraction);
+        }
+        $this->actualresponses[$actualresponse]->increment_count();
+    }
+
+    /**
+     * @param \qubaid_condition $qubaids
+     * @param int               $questionid the question id
+     * @param string            $subpartid
+     */
+    public function cache($qubaids, $questionid, $subpartid) {
+        foreach ($this->actualresponses as $response => $actualresponse) {
+            $actualresponse->cache($qubaids, $questionid, $subpartid, $this->responseclassid, $response);
+        }
+    }
+
+    public function add_response_and_count($response, $fraction, $count) {
+        $this->actualresponses[$response] = new analysis_for_actual_response($response, $fraction, $count);
+    }
+
+    /**
+     * @return bool whether this analysis has a response class with more than one
+     *      different actual response, or if the actual response is different from
+     *      the model response.
+     */
+    public function has_actual_responses() {
+        if (count($this->actualresponses) > 1) {
+            return true;
+        } else if (count($this->actualresponses) == 1) {
+            $onlyactualresponse = reset($this->actualresponses);
+            return !$onlyactualresponse->response_matches($this->modelresponse);
+        }
+        return false;
+    }
+
+    /**
+     * @return object[]
+     */
+    public function data_for_question_response_table($responseclasscolumn, $partid) {
+        $return = array();
+        if (empty($this->actualresponses)) {
+            $rowdata = new \stdClass();
+            $rowdata->part = $partid;
+            $rowdata->responseclass = $this->modelresponse;
+            if (!$responseclasscolumn) {
+                $rowdata->response = $this->modelresponse;
+            } else {
+                $rowdata->response = '';
+            }
+            $rowdata->fraction = $this->fraction;
+            $rowdata->count = 0;
+            $return[] = $rowdata;
+        } else {
+            foreach ($this->actualresponses as $actualresponse) {
+                $return[] = $actualresponse->data_for_question_response_table($partid, $this->modelresponse);
+            }
+        }
+        return $return;
+    }
+}
diff --git a/question/classes/statistics/responses/analysis_for_question.php b/question/classes/statistics/responses/analysis_for_question.php
new file mode 100644 (file)
index 0000000..3d172d2
--- /dev/null
@@ -0,0 +1,141 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains the code to analyse all the responses to a particular
+ * question.
+ *
+ * @package    core
+ * @subpackage questionbank
+ * @copyright  2013 Open University
+ * @author     Jamie Pratt <me@jamiep.org>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_question\statistics\responses;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Analysis for possible responses for parts of a question. It is up to a question type designer to decide on how many parts their
+ * question has. A sub part might represent a sub question embedded in the question for example in a matching question there are
+ * several sub parts. A numeric question with a unit might be divided into two sub parts for the purposes of response analysis
+ * or the question type designer might decide to treat the answer, both the numeric and unit part,
+ * as a whole for the purposes of response analysis.
+ *
+ * Responses can be further divided into 'classes' in which they are classified. One or more of these 'classes' are contained in
+ * the responses
+ *
+ * @copyright 2013 Open University
+ * @author    Jamie Pratt <me@jamiep.org>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+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.
+     *
+     * @param $subparts[string]array[]\question_possible_response $array
+     */
+    public function __construct(array $subparts = null) {
+        if (!is_null($subparts)) {
+            foreach ($subparts as $subpartid => $classes) {
+                $this->subparts[$subpartid] = new analysis_for_subpart($classes);
+            }
+        }
+    }
+
+    /**
+     * @var analysis_for_subpart[]
+     */
+    protected $subparts;
+
+    /**
+     * Unique ids for sub parts.
+     *
+     * @return string[]
+     */
+    public function get_subpart_ids() {
+        return array_keys($this->subparts);
+    }
+
+    /**
+     * @param string $subpartid id for sub part.
+     * @return analysis_for_subpart
+     */
+    public function get_subpart($subpartid) {
+        return $this->subparts[$subpartid];
+    }
+
+    /**
+     * Used to work out what kind of table is needed to display stats.
+     *
+     * @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;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Used to work out what kind of table is needed to display stats.
+     *
+     * @return bool whether this analysis has more than one subpart.
+     */
+    public function has_subparts() {
+        return count($this->subparts) > 1;
+    }
+
+    /**
+     * 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.
+     */
+    public function count_response_parts($responseparts) {
+        foreach ($responseparts as $subpartid => $responsepart) {
+            $this->get_subpart($subpartid)->count_response($responsepart);
+        }
+    }
+
+    /**
+     * @param \qubaid_condition $qubaids
+     * @param int               $questionid the question id
+     */
+    public function cache($qubaids, $questionid) {
+        foreach ($this->subparts as $subpartid => $subpart) {
+            $subpart->cache($qubaids, $questionid, $subpartid);
+        }
+    }
+
+    /**
+     * @return bool whether this analysis has a response class with more than one
+     *      different actual response, or if the actual response is different from
+     *      the model response.
+     */
+    public function has_actual_responses() {
+        foreach ($this->subparts as $subpartid => $subpart) {
+            if ($subpart->has_actual_responses()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+}
diff --git a/question/classes/statistics/responses/analysis_for_subpart.php b/question/classes/statistics/responses/analysis_for_subpart.php
new file mode 100644 (file)
index 0000000..3c2dfd6
--- /dev/null
@@ -0,0 +1,105 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ *
+ * 'Classes' to classify the sub parts of a question response into.
+ *
+ * @package    core
+ * @subpackage questionbank
+ * @copyright  2013 The Open University
+ * @author     James Pratt me@jamiep.org
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_question\statistics\responses;
+
+
+class analysis_for_subpart {
+
+    /**
+     * Takes an array of possible_responses - ({@link \question_possible_response} objects).
+     * Or takes an array of {@link \question_possible_response} objects.
+     *
+     * @param \question_possible_response[] $responseclasses
+     */
+    public function __construct(array $responseclasses = null) {
+        if (is_array($responseclasses)) {
+            foreach ($responseclasses as $responseclassid => $reponseclass) {
+                $this->responseclasses[$responseclassid] = new analysis_for_class($reponseclass, $responseclassid);
+            }
+        }
+    }
+
+    /**
+     *
+     * @var analysis_for_class[]
+     */
+    protected $responseclasses;
+
+    /**
+     * Unique ids for response classes.
+     *
+     * @return string[]
+     */
+    public function get_response_class_ids() {
+        return array_keys($this->responseclasses);
+    }
+
+    /**
+     * @param string $classid id for response class.
+     * @return analysis_for_class
+     */
+    public function get_response_class($classid) {
+        return $this->responseclasses[$classid];
+    }
+
+    public function has_multiple_response_classes() {
+        return count($this->responseclasses) > 1;
+    }
+
+    /**
+     * @param \question_classified_response $subpart
+     */
+    public function count_response($subpart) {
+        $this->responseclasses[$subpart->responseclassid]->count_response($subpart->response, $subpart->fraction);
+    }
+
+    /**
+     * @param \qubaid_condition $qubaids
+     * @param int               $questionid the question id
+     * @param string            $subpartid
+     */
+    public function cache($qubaids, $questionid, $subpartid) {
+        foreach ($this->responseclasses as $responseclassid => $responseclass) {
+            $responseclass->cache($qubaids, $questionid, $subpartid, $responseclassid);
+        }
+    }
+
+    /**
+     * @return bool whether this analysis has a response class with more than one
+     *      different actual response, or if the actual response is different from
+     *      the model response.
+     */
+    public function has_actual_responses() {
+        foreach ($this->responseclasses as $responseclass) {
+            if ($responseclass->has_actual_responses()) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
index e3300c3..1f36349 100644 (file)
@@ -1242,11 +1242,11 @@ class question_attempt {
     }
 
     /**
-     * @return array subpartid => object with fields
-     *      ->responseclassid matches one of the values returned from quetion_type::get_possible_responses.
-     *      ->response the actual response the student gave to this part, as a string.
-     *      ->fraction the credit awarded for this subpart, may be null.
-     *      returns an empty array if no analysis is possible.
+     * Break down a student response by sub part and classification.
+     * See also {@link question_type::get_possible_responses()}
+     * Used for response analysis.
+     *
+     * @return question_possible_response[] where keys are subpartid.
      */
     public function classify_response() {
         return $this->behaviour->classify_response();