From d50b05e69642f099a8fe7530c48879395fbcfbb4 Mon Sep 17 00:00:00 2001 From: Jamie Pratt Date: Fri, 27 Sep 2013 13:50:12 +0700 Subject: [PATCH] MDL-41751 changes to api of question_response_analyser and code refactoring and clean up. --- mod/quiz/report/statistics/report.php | 62 +++----- .../statistics/statistics_question_table.php | 23 +-- question/behaviour/behaviourbase.php | 7 +- .../classes/statistics/responses/analyser.php | 122 +++++---------- .../analysis_for_actual_response.php | 106 +++++++++++++ .../responses/analysis_for_class.php | 141 ++++++++++++++++++ .../responses/analysis_for_question.php | 141 ++++++++++++++++++ .../responses/analysis_for_subpart.php | 105 +++++++++++++ question/engine/questionattempt.php | 10 +- 9 files changed, 571 insertions(+), 146 deletions(-) create mode 100644 question/classes/statistics/responses/analysis_for_actual_response.php create mode 100644 question/classes/statistics/responses/analysis_for_class.php create mode 100644 question/classes/statistics/responses/analysis_for_question.php create mode 100644 question/classes/statistics/responses/analysis_for_subpart.php diff --git a/mod/quiz/report/statistics/report.php b/mod/quiz/report/statistics/report.php index 5179ee88cd5..19fa679f795 100644 --- a/mod/quiz/report/statistics/report.php +++ b/mod/quiz/report/statistics/report.php @@ -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) { diff --git a/mod/quiz/report/statistics/statistics_question_table.php b/mod/quiz/report/statistics/statistics_question_table.php index 4bfc4115ab7..5591e50180a 100644 --- a/mod/quiz/report/statistics/statistics_question_table.php +++ b/mod/quiz/report/statistics/statistics_question_table.php @@ -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) { diff --git a/question/behaviour/behaviourbase.php b/question/behaviour/behaviourbase.php index a5384527237..d8f4b2a1555 100644 --- a/question/behaviour/behaviourbase.php +++ b/question/behaviour/behaviourbase.php @@ -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()); diff --git a/question/classes/statistics/responses/analyser.php b/question/classes/statistics/responses/analyser.php index ae3d206242b..0759c85a953 100644 --- a/question/classes/statistics/responses/analyser.php +++ b/question/classes/statistics/responses/analyser.php @@ -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(); + /** @var integer Time after which responses are automatically reanalysed. */ + const TIME_TO_CACHE = 900; // 15 minutes. - $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; - } - - $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 index 00000000000..d1ff8577feb --- /dev/null +++ b/question/classes/statistics/responses/analysis_for_actual_response.php @@ -0,0 +1,106 @@ +. + +/** + * ${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 index 00000000000..471f6e9cb09 --- /dev/null +++ b/question/classes/statistics/responses/analysis_for_class.php @@ -0,0 +1,141 @@ +. + +/** + * ${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 index 00000000000..3d172d221cb --- /dev/null +++ b/question/classes/statistics/responses/analysis_for_question.php @@ -0,0 +1,141 @@ +. + +/** + * 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 + * @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 + * @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 index 00000000000..3c2dfd69a92 --- /dev/null +++ b/question/classes/statistics/responses/analysis_for_subpart.php @@ -0,0 +1,105 @@ +. + +/** + * + * '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; + } +} diff --git a/question/engine/questionattempt.php b/question/engine/questionattempt.php index e3300c3cb88..1f36349fadd 100644 --- a/question/engine/questionattempt.php +++ b/question/engine/questionattempt.php @@ -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();