MDL-41759 quiz statistics : break down responses for each variant

This commit is contained in:
James Pratt 2013-12-21 15:50:19 +07:00
parent 51e3ded838
commit 4922e79f3e
13 changed files with 332 additions and 92 deletions

View File

@ -1499,6 +1499,7 @@
<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"/>

View 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;

View 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}';

View 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);
}
}
}
}
}
}

View 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')));

View File

@ -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
1 slot randq variant subpart modelresponse actualresponse count
2 1 numerical 1 1 3.142 (3.142..3.142) 3.142 1
3 1 numerical 1 1 3.14 (3.14..3.14) 3.14 7
4 1 numerical 1 1 3.1 (3.1..3.1) 3.1 4
5 1 shortanswer 1 1 frog frog 9
6 1 shortanswer 1 1 * butterfly 2
7 1 shortanswer 1 1 toad toad 2
8 2 1 1 {a} + {b} (±0.01 Relative) 9.9 3
9 2 1 1 [NO MATCH] -0.7 1
10 2 1 1 [NO MATCH] -0.2 1
11 2 1 1 [NO MATCH] -1 1
12 2 4 1 {a} + {b} (±0.01 Relative) 19.4 2
13 2 4 1 [NO RESPONSE] 1
14 2 4 1 [NO MATCH] -0.4 1
15 3 1 1 frog: amphibian amphibian 25
16 3 1 2 cat: mammal mammal 24
17 3 1 2 cat: amphibian amphibian 1
18 3 1 3 newt: amphibian amphibian 24
19 3 1 3 newt: mammal mammal 1

View 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,12 +182,66 @@ 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);
}
/**
* Check that the stat is as expected within a reasonable tolerance.
*

View 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;

View 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;

View 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);
}
}

View 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 analysis_for_subpart[]
* @var array[] See description above in constructor method.
*/
protected $subparts;
protected $possibleresponses = array();
/**
* Unique ids for sub parts.
* A multidimensional array whose first index is variant no and second index is subpart id, array contents are of type
* {@link analysis_for_subpart}.
*
* @return string[]
* @var array[]
*/
public function get_subpart_ids() {
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);
}
}
/**
* Variant nos found in this question's attempt data.
*
* @return int[]
*/
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($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;

View 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);
}
}

View 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.