mirror of
https://github.com/moodle/moodle.git
synced 2025-01-19 06:18:28 +01:00
MDL-41759 quiz statistics : break down responses for each variant
This commit is contained in:
parent
51e3ded838
commit
4922e79f3e
@ -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"/>
|
||||
|
@ -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;
|
||||
|
@ -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}';
|
||||
|
||||
|
@ -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.' '.$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'] . ' ' .
|
||||
question_bank::get_qtype($questionstat->question->qtype, false)->menu_name() . ' ' .
|
||||
@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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')));
|
||||
|
19
mod/quiz/report/statistics/tests/fixtures/responsecounts00.csv
vendored
Normal file
19
mod/quiz/report/statistics/tests/fixtures/responsecounts00.csv
vendored
Normal 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
|
|
@ -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.
|
||||
*
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user