From c3e2e754dd4ce93ed9542c363b86e78e9d04313b Mon Sep 17 00:00:00 2001 From: James Pratt Date: Wed, 11 Dec 2013 16:59:27 +0700 Subject: [PATCH] MDL-43338 quiz statistics : refactoring question statistics code to improve readability and maintainability. --- mod/quiz/report/statistics/report.php | 174 +++---- .../report/statistics/statistics_table.php | 9 +- .../statistics/tests/statistics_test.php | 25 +- .../stats_from_steps_walkthrough_test.php | 24 +- .../all_calculated_for_qubaid_condition.php | 445 ++++++++++++++++++ .../statistics/questions/calculated.php | 51 +- .../questions/calculated_for_subquestion.php | 13 + .../statistics/questions/calculator.php | 295 ++++-------- 8 files changed, 697 insertions(+), 339 deletions(-) create mode 100644 question/classes/statistics/questions/all_calculated_for_qubaid_condition.php diff --git a/mod/quiz/report/statistics/report.php b/mod/quiz/report/statistics/report.php index 148ea39bf8c..a0101c505b0 100644 --- a/mod/quiz/report/statistics/report.php +++ b/mod/quiz/report/statistics/report.php @@ -38,8 +38,6 @@ require_once($CFG->dirroot . '/mod/quiz/report/statistics/statisticslib.php'); */ class quiz_statistics_report extends quiz_default_report { - const SUBQ_AND_VARIANT_ROW_LIMIT = 10; - /** * @var context_module */ @@ -145,13 +143,12 @@ class quiz_statistics_report extends quiz_default_report { if (!$nostudentsingroup) { // Get the data to be displayed. $progress = $this->get_progress_trace_instance(); - list($quizstats, $questionstats, $subquestionstats) = + list($quizstats, $questionstats) = $this->get_all_stats_and_analysis($quiz, $whichattempts, $groupstudents, $questions, $progress); } else { // Or create empty stats containers. $quizstats = new \quiz_statistics\calculated($whichattempts); - $questionstats = array(); - $subquestionstats = array(); + $questionstats = new \core_question\statistics\questions\all_calculated_for_qubaid_condition(); } // Set up the table, if there is data. @@ -173,6 +170,10 @@ class quiz_statistics_report extends quiz_default_report { echo $OUTPUT->notification(get_string('noattempts', 'quiz')); } + foreach($questionstats->any_error_messages() as $errormessage) { + echo $OUTPUT->notification($errormessage); + } + // Print display options form. $mform->display(); } @@ -183,7 +184,7 @@ class quiz_statistics_report extends quiz_default_report { $this->download_quiz_info_table($quizinfo); if ($quizstats->s()) { - $this->output_quiz_structure_analysis_table($quizstats->s(), $questionstats, $subquestionstats); + $this->output_quiz_structure_analysis_table($questionstats); if ($this->table->is_downloading() == 'xhtml' && $quizstats->s() != 0) { $this->output_statistics_graph($quiz->id, $currentgroup, $whichattempts); @@ -193,13 +194,14 @@ class quiz_statistics_report extends quiz_default_report { if (question_bank::get_qtype( $question->qtype, false)->can_analyse_responses()) { $this->output_individual_question_response_analysis( - $question, $questionstats[$slot]->s, $reporturl, $qubaids); + $question, $questionstats->for_slot($slot)->s, $reporturl, $qubaids); - } else if (!empty($questionstats[$slot]->subquestions)) { - $subitemstodisplay = explode(',', $questionstats[$slot]->subquestions); - foreach ($subitemstodisplay as $subitemid) { - $this->output_individual_question_response_analysis( - $subquestionstats[$subitemid]->question, $subquestionstats[$subitemid]->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); } } } @@ -213,8 +215,11 @@ class quiz_statistics_report extends quiz_default_report { print_error('questiondoesnotexist', 'question'); } - $this->output_individual_question_data($quiz, $questionstats[$slot]); - $this->output_individual_question_response_analysis($questions[$slot], $questionstats[$slot]->s, $reporturl, $qubaids); + $this->output_individual_question_data($quiz, $questionstats->for_slot($slot)); + $this->output_individual_question_response_analysis($questions[$slot], + $questionstats->for_slot($slot)->s, + $reporturl, + $qubaids); // Back to overview link. echo $OUTPUT->box('' . @@ -223,13 +228,15 @@ class quiz_statistics_report extends quiz_default_report { } else if ($qid) { // Report on an individual sub-question indexed questionid. - if (!isset($subquestionstats[$qid])) { + if (is_null($questionstats->for_subq($qid))) { print_error('questiondoesnotexist', 'question'); } - $this->output_individual_question_data($quiz, $subquestionstats[$qid]); - $this->output_individual_question_response_analysis($subquestionstats[$qid]->question, - $subquestionstats[$qid]->s, $reporturl, $qubaids); + $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('' . @@ -240,7 +247,9 @@ class quiz_statistics_report extends quiz_default_report { // 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); + if ($quizstats->s()) { + $this->output_quiz_structure_analysis_table($questionstats); + } $this->table->finish_output(); } else { @@ -252,7 +261,7 @@ class quiz_statistics_report extends quiz_default_report { echo $this->output_quiz_info_table($quizinfo); if ($quizstats->s()) { echo $OUTPUT->heading(get_string('quizstructureanalysis', 'quiz_statistics'), 3); - $this->output_quiz_structure_analysis_table($quizstats->s(), $questionstats, $subquestionstats); + $this->output_quiz_structure_analysis_table($questionstats); $this->output_statistics_graph($quiz->id, $currentgroup, $whichattempts); } } @@ -402,100 +411,25 @@ class quiz_statistics_report extends quiz_default_report { /** * Output the table that lists all the questions in the quiz with their statistics. - * @param int $s number of attempts. - * @param \core_question\statistics\questions\calculated[] $questionstats the stats for the main questions in the quiz. - * @param \core_question\statistics\questions\calculated_for_subquestion[] $subquestionstats the stats of any random questions. + * @param \core_question\statistics\questions\all_calculated_for_qubaid_condition $questionstats the stats for all questions in + * the quiz including subqs and + * variants. */ - protected function output_quiz_structure_analysis_table($s, $questionstats, $subquestionstats) { - if (!$s) { - return; - } - - foreach ($questionstats as $questionstat) { + protected function output_quiz_structure_analysis_table($questionstats) { + foreach ($questionstats->get_all_slots() as $slot) { // Output the data for these question statistics. - $this->table->add_data_keyed($this->table->format_row($questionstat)); - if (count($questionstat->variantstats) > 1) { - if (count($questionstat->variantstats) > static::SUBQ_AND_VARIANT_ROW_LIMIT) { - $statstoadd = $this->find_min_median_and_max_facility_stats_objects($questionstat->variantstats); - } else { - ksort($questionstat->variantstats); - $statstoadd = $questionstat->variantstats; - } - $this->add_array_of_rows_to_table($statstoadd); - } + $this->table->add_data_keyed($this->table->format_row($questionstats->for_slot($slot))); - if (empty($questionstat->subquestions)) { - continue; - } - - // And its sub-questions, if it has any. - $subitemstodisplay = explode(',', $questionstat->subquestions); - - // We need to get all variants out of sub-questions to count them and possibly find min, median and max. - $displayorder = 1; - $subqvariants = array(); - foreach ($subitemstodisplay as $subitemid) { - if (count($subquestionstats[$subitemid]->variantstats) > 1) { - ksort($subquestionstats[$subitemid]->variantstats); - foreach ($subquestionstats[$subitemid]->variantstats as $variantstat) { - $variantstat->subqdisplayorder = $displayorder; - $variantstat->question->number = $questionstat->question->number; - $subqvariants[] = $variantstat; - } - } - $displayorder++; - } - if (count($subqvariants) > static::SUBQ_AND_VARIANT_ROW_LIMIT) { - // Too many variants from randomly selected questions. - $toadd = $this->find_min_median_and_max_facility_stats_objects($subqvariants); - $this->add_array_of_rows_to_table($toadd); - } else if (count($subitemstodisplay) > static::SUBQ_AND_VARIANT_ROW_LIMIT) { - // Too many randomly selected questions. - $toadd = $this->find_min_median_and_max_facility_stats_objects($subitemstodisplay); - $this->add_array_of_rows_to_table($toadd); - } else { - foreach ($subitemstodisplay as $subitemid) { - $subquestionstats[$subitemid]->maxmark = $questionstat->maxmark; - $subquestionstats[$subitemid]->subqdisplayorder = $displayorder; - $subquestionstats[$subitemid]->question->number = $questionstat->question->number; - $this->table->add_data_keyed($this->table->format_row($subquestionstats[$subitemid])); - if (count($subquestionstats[$subitemid]->variantstats) > 1) { - ksort($subquestionstats[$subitemid]->variantstats); - foreach ($subquestionstats[$subitemid]->variantstats as $variantstat) { - $this->table->add_data_keyed($this->table->format_row($variantstat)); - } - } - } - } + $limitvariants = !$this->table->is_downloading(); + $this->add_array_of_rows_to_table($questionstats->all_subq_and_variant_stats_for_slot($slot, $limitvariants)); } $this->table->finish_output(!$this->table->is_downloading()); } - protected function find_min_median_and_max_facility_stats_objects($questionstats) { - $facilities = array(); - foreach ($questionstats as $key => $questionstat) { - $facilities[$key] = (float)$questionstat->facility; - } - asort($facilities); - $facilitykeys = array_keys($facilities); - $keyformin = $facilitykeys[0]; - $keyformedian = $facilitykeys[(int)(round(count($facilitykeys) / 2)-1)]; - $keyformax = $facilitykeys[count($facilitykeys) - 1]; - $toreturn = array(); - foreach (array($keyformin => 'minimumfacility', - $keyformedian => 'medianfacility', - $keyformax => 'maximumfacility') as $key => $stringid) { - $questionstats[$key]->minmedianmaxnotice = get_string($stringid, 'quiz_statistics'); - $toreturn[] = $questionstats[$key]; - } - return $toreturn; - } - - /** - * @param \core_question\statistics\questions\calculator $statstoadd + * @param \core_question\statistics\questions\calculated[] $statstoadd */ protected function add_array_of_rows_to_table($statstoadd) { foreach ($statstoadd as $stattoadd) { @@ -580,10 +514,9 @@ class quiz_statistics_report extends quiz_default_report { * we calculate stats based on which attempts would affect the grade for each student. * @param array $groupstudents students in this group. * @param array $questions full question data. - * @return array with 4 elements: - * - $quizstats The statistics for overall attempt scores. - * - $questionstats array of \core_question\statistics\questions\calculated objects keyed by slot. - * - $subquestionstats array of \core_question\statistics\questions\calculated_for_subquestion objects keyed by question id. + * @param \core\progress\base|null $progress + * @return array with 2 elements: - $quizstats The statistics for overall attempt scores. + * - $questionstats \core_question\statistics\questions\all_calculated_for_qubaid_condition */ public function get_all_stats_and_analysis($quiz, $whichattempts, $groupstudents, $questions, $progress = null) { @@ -602,23 +535,24 @@ class quiz_statistics_report extends quiz_default_report { $progress->start_progress('', 3); // Recalculate now. - list($questionstats, $subquestionstats) = $qcalc->calculate($qubaids); + $questionstats = $qcalc->calculate($qubaids); $progress->progress(1); $quizstats = $quizcalc->calculate($quiz->id, $whichattempts, $groupstudents, count($questions), $qcalc->get_sum_of_mark_variance()); $progress->progress(2); if ($quizstats->s()) { - $this->analyse_responses_for_all_questions_and_subquestions($qubaids, $questions, $subquestionstats, $progress); + $subquestions = $questionstats->get_sub_questions(); + $this->analyse_responses_for_all_questions_and_subquestions($qubaids, $questions, $subquestions, $progress); } $progress->progress(3); $progress->end_progress(); } else { $quizstats = $quizcalc->get_cached($qubaids); - list($questionstats, $subquestionstats) = $qcalc->get_cached($qubaids); + $questionstats = $qcalc->get_cached($qubaids); } - return array($quizstats, $questionstats, $subquestionstats); + return array($quizstats, $questionstats); } /** @@ -638,7 +572,7 @@ class quiz_statistics_report extends quiz_default_report { return $this->progress; } - protected function analyse_responses_for_all_questions_and_subquestions($qubaids, $questions, $subquestionstats, + protected function analyse_responses_for_all_questions_and_subquestions($qubaids, $questions, $subquestions, $progress = null) { if ($progress === null) { @@ -646,7 +580,7 @@ class quiz_statistics_report extends quiz_default_report { } // Starting response analysis tasks. - $progress->start_progress('', count($questions) + count($subquestionstats)); + $progress->start_progress('', count($questions) + count($subquestions)); // Starting response analysis of main questions. $progress->start_progress('', count($questions), count($questions)); @@ -667,19 +601,19 @@ class quiz_statistics_report extends quiz_default_report { $progress->end_progress(); // Starting response analysis of sub-questions. - $countsubquestions = count($subquestionstats); + $countsubquestions = count($subquestions); $progress->start_progress('', $countsubquestions, $countsubquestions); $donecount = 1; - foreach ($subquestionstats as $subquestionstat) { + foreach ($subquestions as $subquestion) { $progress->progress($donecount); $donecount++; - if (!question_bank::get_qtype($subquestionstat->question->qtype, false)->can_analyse_responses() || - isset($done[$subquestionstat->question->id])) { + if (!question_bank::get_qtype($subquestion->qtype, false)->can_analyse_responses() || + isset($done[$subquestion->id])) { continue; } - $done[$subquestionstat->question->id] = 1; + $done[$subquestion->id] = 1; - $responesstats = new \core_question\statistics\responses\analyser($subquestionstat->question); + $responesstats = new \core_question\statistics\responses\analyser($subquestion); $responesstats->calculate($qubaids); } // Finished sub-question tasks. diff --git a/mod/quiz/report/statistics/statistics_table.php b/mod/quiz/report/statistics/statistics_table.php index 14496161d89..45bbcc6e0c5 100644 --- a/mod/quiz/report/statistics/statistics_table.php +++ b/mod/quiz/report/statistics/statistics_table.php @@ -28,7 +28,9 @@ require_once($CFG->libdir.'/tablelib.php'); /** * This table has one row for each question in the quiz, with sub-rows when - * random questions appear. There are columns for the various statistics. + * random questions and variants appear. + * + * There are columns for the various item and position statistics. * * @copyright 2008 Jamie Pratt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later @@ -136,6 +138,9 @@ class quiz_statistics_table extends flexible_table { * @return string contents of this table cell. */ protected function col_number($questionstat) { + if (!isset($questionstat->question->number)) { + return ''; + } $number = $questionstat->question->number; if (isset($questionstat->subqdisplayorder)) { @@ -214,7 +219,7 @@ class quiz_statistics_table extends flexible_table { } if (!empty($questionstat->minmedianmaxnotice)) { - $name = $questionstat->minmedianmaxnotice . '
' . $name; + $name = get_string($questionstat->minmedianmaxnotice, 'quiz_statistics') . '
' . $name; } return $name; diff --git a/mod/quiz/report/statistics/tests/statistics_test.php b/mod/quiz/report/statistics/tests/statistics_test.php index 6ba5284fba3..3f5e0905615 100644 --- a/mod/quiz/report/statistics/tests/statistics_test.php +++ b/mod/quiz/report/statistics/tests/statistics_test.php @@ -30,6 +30,18 @@ require_once($CFG->libdir . '/questionlib.php'); require_once($CFG->dirroot . '/mod/quiz/locallib.php'); require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php'); +class testable_all_calculated_for_qubaid_condition extends \core_question\statistics\questions\all_calculated_for_qubaid_condition { + + /** + * Disabling caching in tests so we are always sure to force the calculation of stats right then and there. + * + * @param qubaid_condition $qubaids + */ + public function cache($qubaids) { + + } +} + /** * Test helper subclass of question_statistics * @@ -43,6 +55,8 @@ class testable_question_statistics extends \core_question\statistics\questions\c */ protected $lateststeps; + protected $statscollectionclassname = 'testable_all_calculated_for_qubaid_condition'; + public function set_step_data($states) { $this->lateststeps = $states; } @@ -86,7 +100,7 @@ class testable_question_statistics extends \core_question\statistics\questions\c * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class quiz_statistics_question_stats_testcase extends basic_testcase { - /** @var qstats object created to test class. */ + /** @var testable_all_calculated_for_qubaid_condition object created to test class. */ protected $qstats; public function test_qstats() { @@ -99,7 +113,7 @@ class quiz_statistics_question_stats_testcase extends basic_testcase { $questions = $this->get_records_from_csv(__DIR__.'/fixtures/mdl_question.csv'); $calculator = new testable_question_statistics($questions); $calculator->set_step_data($steps); - list($this->qstats, ) = $calculator->calculate(null); + $this->qstats = $calculator->calculate(null); // Values expected are taken from contrib/tools/quiz_tools/stats.xls. $facility = array(0, 0, 0, 0, null, null, null, 41.19318182, 81.36363636, @@ -127,13 +141,12 @@ class quiz_statistics_question_stats_testcase extends basic_testcase { } public function qstats_q_fields($fieldname, $values, $multiplier=1) { - foreach ($this->qstats as $qstat) { + foreach ($this->qstats->get_all_slots() as $slot) { $value = array_shift($values); if ($value !== null) { - $this->assertEquals($value, $qstat->{$fieldname} * $multiplier, - '', 1E-6); + $this->assertEquals($value, $this->qstats->for_slot($slot)->{$fieldname} * $multiplier, '', 1E-6); } else { - $this->assertEquals($value, $qstat->{$fieldname} * $multiplier); + $this->assertEquals($value, $this->qstats->for_slot($slot)->{$fieldname} * $multiplier); } } } diff --git a/mod/quiz/report/statistics/tests/stats_from_steps_walkthrough_test.php b/mod/quiz/report/statistics/tests/stats_from_steps_walkthrough_test.php index 171aeba0c1d..b9d8306a397 100644 --- a/mod/quiz/report/statistics/tests/stats_from_steps_walkthrough_test.php +++ b/mod/quiz/report/statistics/tests/stats_from_steps_walkthrough_test.php @@ -80,7 +80,7 @@ class quiz_report_statistics_from_steps_testcase extends mod_quiz_attempt_walkth $whichattempts = QUIZ_GRADEAVERAGE; $groupstudents = array(); $questions = $this->report->load_and_initialise_questions_for_calculations($this->quiz); - list($quizstats, $questionstats, $subquestionstats) = + list($quizstats, $questionstats) = $this->report->get_all_stats_and_analysis($this->quiz, $whichattempts, $groupstudents, $questions); $qubaids = quiz_statistics_qubaids_condition($this->quiz->id, $groupstudents, $whichattempts); @@ -102,11 +102,9 @@ class quiz_report_statistics_from_steps_testcase extends mod_quiz_attempt_walkth $this->assertTimeCurrent($responesstats->get_last_analysed_time($qubaids)); } - // These quiz stats and the question stats found in qstats00.csv were calculated independently in spreadsheets which are + // 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 - - // These quiz stats and the position stats here are calculated in stats.xls and stats.ods available, see above github URL. $quizstatsexpected = array( 'median' => 4.5, 'firstattemptsavg' => 4.617333332, @@ -129,8 +127,7 @@ class quiz_report_statistics_from_steps_testcase extends mod_quiz_attempt_walkth $slotqstats = $csvdata['qstats']->getRow($rowno); foreach ($slotqstats as $statname => $slotqstat) { if ($statname !== 'slot') { - $this->assert_stat_equals($questionstats, $subquestionstats, $slotqstats['slot'], - null, null, $statname, (float)$slotqstat); + $this->assert_stat_equals($questionstats, $slotqstats['slot'], null, null, $statname, (float)$slotqstat); } } } @@ -146,7 +143,7 @@ class quiz_report_statistics_from_steps_testcase extends mod_quiz_attempt_walkth 'slot' => null, 'subquestion' => true); foreach ($itemstats as $statname => $expected) { - $this->assert_stat_equals($questionstats, $subquestionstats, 1, null, 'numerical', $statname, $expected); + $this->assert_stat_equals($questionstats, 1, null, 'numerical', $statname, $expected); } @@ -176,7 +173,7 @@ class quiz_report_statistics_from_steps_testcase extends mod_quiz_attempt_walkth 'subquestion' => false)); foreach ($statsforslot2variants as $variant => $stats) { foreach ($stats as $statname => $expected) { - $this->assert_stat_equals($questionstats, $subquestionstats, 2, $variant, null, $statname, $expected); + $this->assert_stat_equals($questionstats, 2, $variant, null, $statname, $expected); } } } @@ -184,22 +181,21 @@ class quiz_report_statistics_from_steps_testcase extends mod_quiz_attempt_walkth /** * Check that the stat is as expected within a reasonable tolerance. * - * @param \core_question\statistics\questions\calculated[] $questionstats - * @param \core_question\statistics\questions\calculated_for_subquestion[] $subquestionstats + * @param \core_question\statistics\questions\all_calculated_for_qubaid_condition $questionstats * @param int $slot * @param int|null $variant if null then not a variant stat. * @param string|null $subqname if null then not an item stat. * @param string $statname * @param float $expected */ - protected function assert_stat_equals($questionstats, $subquestionstats, $slot, $variant, $subqname, $statname, $expected) { + protected function assert_stat_equals($questionstats, $slot, $variant, $subqname, $statname, $expected) { if ($variant === null && $subqname === null) { - $actual = $questionstats[$slot]->{$statname}; + $actual = $questionstats->for_slot($slot)->{$statname}; } else if ($subqname !== null) { - $actual = $subquestionstats[$this->randqids[$slot][$subqname]]->{$statname}; + $actual = $questionstats->for_subq($this->randqids[$slot][$subqname])->{$statname}; } else { - $actual = $questionstats[$slot]->variantstats[$variant]->{$statname}; + $actual = $questionstats->for_slot($slot, $variant)->{$statname}; } if (is_bool($expected) || is_string($expected)) { $this->assertEquals($expected, $actual, "$statname for slot $slot"); diff --git a/question/classes/statistics/questions/all_calculated_for_qubaid_condition.php b/question/classes/statistics/questions/all_calculated_for_qubaid_condition.php new file mode 100644 index 00000000000..4dca155d66d --- /dev/null +++ b/question/classes/statistics/questions/all_calculated_for_qubaid_condition.php @@ -0,0 +1,445 @@ +. + +/** + * A collection of all the question statistics calculated for an activity instance ie. the stats calculated for slots and + * sub-questions and variants of those questions. + * + * @package core_question + * @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\questions; + +/** + * A collection of all the question statistics calculated for an activity instance. + * + * @package core_question + */ +class all_calculated_for_qubaid_condition { + + /** + * The limit of rows of sub-question and variants rows to display on main page of report before switching to showing min, + * median and max variants. + */ + const SUBQ_AND_VARIANT_ROW_LIMIT = 10; + + /** + * @var object[] + */ + public $subquestions; + + /** + * Holds slot (position) stats and stats for variants of questions in slots. + * + * @var calculated[] + */ + public $questionstats = array(); + + /** + * Holds sub-question stats and stats for variants of subqs. + * + * @var calculated_for_subquestion[] + */ + public $subquestionstats = array(); + + /** + * Set up a calculated_for_subquestion instance ready to store a randomly selected question's stats. + * + * @param object $step + * @param int|null $variant Is this to keep track of a variant's stats? If so what is the variant, if not null. + */ + public function initialise_for_subq($step, $variant = null) { + $newsubqstat = new calculated_for_subquestion($step, $variant); + if ($variant === null) { + $this->subquestionstats[$step->questionid] = $newsubqstat; + } else { + $this->subquestionstats[$step->questionid]->variantstats[$variant] = $newsubqstat; + } + } + + /** + * Set up a calculated instance ready to store a slot question's stats. + * + * @param int $slot + * @param object $question + * @param int|null $variant Is this to keep track of a variant's stats? If so what is the variant, if not null. + */ + public function initialise_for_slot($slot, $question, $variant = null) { + $newqstat = new calculated($question, $slot, $variant); + if ($variant === null) { + $this->questionstats[$slot] = $newqstat; + } else { + $this->questionstats[$slot]->variantstats[$variant] = $newqstat; + } + } + + /** + * Reference for a item stats instance for a questionid and optional variant no. + * + * @param $questionid + * @param int|null $variant if not null then we want the object to store a variant of a sub-question's stats. + * @return calculated_for_subquestion|null null if the stats object does not yet exist. + */ + public function for_subq($questionid, $variant = null) { + if ($variant === null) { + if (!isset($this->subquestionstats[$questionid])) { + return null; + } else { + return $this->subquestionstats[$questionid]; + } + } else { + if (!isset($this->subquestionstats[$questionid]->variantstats[$variant])) { + return null; + } else { + return $this->subquestionstats[$questionid]->variantstats[$variant]; + } + } + } + + /** + * ids of all randomly selected question for all slots. + * + * @return int[] An array of all sub-question ids. + */ + public function get_all_subq_ids() { + return array_keys($this->subquestionstats); + } + + /** + * All slots nos that stats have been calculated for. + * + * @return int[] An array of all slot nos. + */ + public function get_all_slots() { + return array_keys($this->questionstats); + } + + /** + * Array of variants of one randomly selected question that have appeared in the attempt data. + * + * @param $questionid + * @return int[] + */ + public function get_variants_for_subq($questionid) { + if (count($this->subquestionstats[$questionid]->variantstats) > 1) { + return array_keys($this->subquestionstats[$questionid]->variantstats); + } else { + return false; + } + } + + /** + * Array of variants that have appeared in the attempt data for a question in one slot. + * + * @param $slot + * @return int[] + */ + public function get_variants_for_slot($slot) { + if (count($this->questionstats[$slot]->variantstats) > 1) { + return array_keys($this->questionstats[$slot]->variantstats); + } else { + return false; + } + } + + /** + * Reference to position stats instance for a slot and optional variant no. + * + * @param $slot + * @param null $variant if provided then we want the object which stores a variant of a position's stats. + * @return calculated|null + */ + public function for_slot($slot, $variant = null) { + if ($variant === null) { + if (!isset($this->questionstats[$slot])) { + return null; + } else { + return $this->questionstats[$slot]; + } + } else { + if (!isset($this->questionstats[$slot]->variantstats[$variant])) { + return null; + } else { + return $this->questionstats[$slot]->variantstats[$variant]; + } + } + } + + /** + * Load cached statistics from the database. + * + * @param $qubaids \qubaid_condition + */ + public function get_cached($qubaids) { + global $DB; + + $timemodified = time() - self::TIME_TO_CACHE; + $questionstatrecs = $DB->get_records_select('question_statistics', 'hashcode = ? AND timemodified > ?', + array($qubaids->get_hash_code(), $timemodified)); + + $questionids = array(); + foreach ($questionstatrecs as $fromdb) { + if (is_null($fromdb->variant) && !$fromdb->slot) { + $questionids[] = $fromdb->questionid; + } + } + $this->subquestions = question_load_questions($questionids); + foreach ($questionstatrecs as $fromdb) { + if (is_null($fromdb->variant)) { + if ($fromdb->slot) { + $this->questionstats[$fromdb->slot]->populate_from_record($fromdb); + // Array created in constructor and populated from question. + } else { + $this->subquestionstats[$fromdb->questionid] = new calculated_for_subquestion(); + $this->subquestionstats[$fromdb->questionid]->populate_from_record($fromdb); + $this->subquestionstats[$fromdb->questionid]->question = $this->subquestions[$fromdb->questionid]; + } + } + } + // Add cached variant stats to data structure. + foreach ($questionstatrecs as $fromdb) { + if (!is_null($fromdb->variant)) { + if ($fromdb->slot) { + $newcalcinstance = new calculated(); + $this->questionstats[$fromdb->slot]->variantstats[$fromdb->variant] = $newcalcinstance; + $newcalcinstance->question = $this->questionstats[$fromdb->slot]->question; + } else { + $newcalcinstance = new calculated_for_subquestion(); + $this->subquestionstats[$fromdb->questionid]->variantstats[$fromdb->variant] = $newcalcinstance; + $newcalcinstance->question = $this->subquestions[$fromdb->questionid]; + } + $newcalcinstance->populate_from_record($fromdb); + } + } + } + + /** + * Find time of non-expired statistics in the database. + * + * @param $qubaids \qubaid_condition + * @return int|bool Time of cached record that matches this qubaid_condition or false is non found. + */ + public function get_last_calculated_time($qubaids) { + global $DB; + + $timemodified = time() - self::TIME_TO_CACHE; + return $DB->get_field_select('question_statistics', 'timemodified', 'hashcode = ? AND timemodified > ?', + array($qubaids->get_hash_code(), $timemodified), IGNORE_MULTIPLE); + } + + /** @var integer Time after which statistics are automatically recomputed. */ + const TIME_TO_CACHE = 900; // 15 minutes. + + /** + * Save stats to db. + * + * @param $qubaids \qubaid_condition + */ + public function cache($qubaids) { + foreach ($this->get_all_slots() as $slot) { + $this->for_slot($slot)->cache($qubaids); + } + + foreach ($this->get_all_subq_ids() as $subqid) { + $this->for_subq($subqid)->cache($qubaids); + } + } + + /** + * Return all sub-questions used. + * + * @return \object[] array of questions. + */ + public function get_sub_questions() { + return $this->subquestions; + } + + /** + * Are there too many rows of sub-questions and / or variant rows. + * + * @param array $rows the rows we intend to add. + * @return bool + */ + protected function too_many_subq_and_or_variant_rows($rows) { + return (count($rows) > static::SUBQ_AND_VARIANT_ROW_LIMIT); + } + + /** + * From a number of calculated instances find the three instances with min, median and maximum facility index values. + * + * @param calculated[] $questionstats + * @return calculated[] 3 stat objects with minimum, median and maximum facility index. + */ + protected function find_min_median_and_max_facility_stats_objects($questionstats) { + $facilities = array(); + foreach ($questionstats as $key => $questionstat) { + $facilities[$key] = (float)$questionstat->facility; + } + asort($facilities); + $facilitykeys = array_keys($facilities); + $keyformin = $facilitykeys[0]; + $keyformedian = $facilitykeys[(int)(round(count($facilitykeys) / 2)-1)]; + $keyformax = $facilitykeys[count($facilitykeys) - 1]; + $toreturn = array(); + foreach (array($keyformin => 'minimumfacility', + $keyformedian => 'medianfacility', + $keyformax => 'maximumfacility') as $key => $stringid) { + $questionstats[$key]->minmedianmaxnotice = $stringid; + $toreturn[] = $questionstats[$key]; + } + return $toreturn; + } + + /** + * Return all stats for variants of question in slot $slot. + * + * @param int $slot + * @return calculated[] + */ + protected function all_variant_stats_for_one_slot($slot) { + $toreturn = array(); + if ($variants = $this->get_variants_for_slot($slot)){ + foreach ($variants as $variant) { + $toreturn[] = $this->for_slot($slot, $variant); + } + } + return $toreturn; + } + + /** + * Return all stats for variants of randomly selected questions for one slot $slot. + * + * @param int $slot + * @return calculated[] + */ + protected function all_subq_variants_for_one_slot($slot) { + $toreturn = array(); + $displayorder = 1; + foreach ($this->for_slot($slot)->get_sub_question_ids() as $subqid) { + if ($variants = $this->get_variants_for_subq($subqid)) { + foreach ($variants as $variant) { + $toreturn[] = $this->make_new_subq_stat_for($displayorder, $slot, $subqid, $variant); + } + } + $displayorder++; + } + return $toreturn; + } + + /** + * Return all stats for randomly selected questions for one slot $slot. + * + * @param int $slot + * @return calculated[] + */ + protected function all_subqs_for_one_slot($slot) { + $displayorder = 1; + $toreturn = array(); + foreach ($this->for_slot($slot)->get_sub_question_ids() as $subqid) { + $toreturn[] = $this->make_new_subq_stat_for($displayorder, $slot, $subqid); + $displayorder++; + } + return $toreturn; + } + + /** + * Return all variant or 'sub-question' stats one slot, either : + * - variants of question + * - variants of randomly selected questions + * - randomly selected questions + * + * @param int $slot + * @param bool $limited + * @return calculated[] + */ + public function all_subq_and_variant_stats_for_slot($slot, $limited) { + // Random question in this slot? + if ($this->for_slot($slot)->get_sub_question_ids()) { + if ($limited) { + $subqvariantstats = $this->all_subq_variants_for_one_slot($slot); + if ($this->too_many_subq_and_or_variant_rows($subqvariantstats)) { + // Too many variants from randomly selected questions. + return $this->find_min_median_and_max_facility_stats_objects($subqvariantstats); + } + $subqstats = $this->all_subqs_for_one_slot($slot); + if ($this->too_many_subq_and_or_variant_rows($subqstats)) { + // Too many randomly selected questions. + return $this->find_min_median_and_max_facility_stats_objects($subqstats); + } + } + $toreturn = array(); + $displaynumber = 1; + foreach ($this->for_slot($slot)->get_sub_question_ids() as $subqid) { + $toreturn[] = $this->make_new_subq_stat_for($displaynumber, $slot, $subqid); + if ($variants = $this->get_variants_for_subq($subqid)) { + foreach ($variants as $variant) { + $toreturn[] = $this->make_new_subq_stat_for($displaynumber, $slot, $subqid, $variant); + } + } + $displaynumber++; + } + return $toreturn; + } else { + $variantstats = $this->all_variant_stats_for_one_slot($slot); + if ($limited && $this->too_many_subq_and_or_variant_rows($variantstats)) { + return $this->find_min_median_and_max_facility_stats_objects($variantstats); + } else { + return $variantstats; + } + } + + } + + /** + * We need a new object for display. Sub-question stats can appear more than once in different slots. + * So we create a clone of the object and then we can set properties on the object that are per slot. + * + * @param $displaynumber + * @param $slot + * @param $subqid + * @param null $variant + * @return calculated_for_subquestion|null + */ + protected function make_new_subq_stat_for($displaynumber, $slot, $subqid, $variant = null) { + $slotstat = fullclone($this->for_subq($subqid, $variant)); + $slotstat->question->number = $this->for_slot($slot)->question->number; + $slotstat->subqdisplayorder = $displaynumber; + return $slotstat; + } + + /** + * Call after calculations to output any error messages. + * + * @return string[] Array of strings describing error messages found during stats calculation. + */ + public function any_error_messages() { + $errors = array(); + foreach ($this->get_all_slots() as $slot) { + foreach ($this->for_slot($slot)->get_sub_question_ids() as $subqid) { + if ($this->for_subq($subqid)->differentweights) { + $name = $this->for_subq($subqid)->question->name; + $errors[] = get_string('erroritemappearsmorethanoncewithdifferentweight', 'quiz_statistics', $name); + } + } + } + return $errors; + + } + +} diff --git a/question/classes/statistics/questions/calculated.php b/question/classes/statistics/questions/calculated.php index 198212a645c..2b4defc3fc5 100644 --- a/question/classes/statistics/questions/calculated.php +++ b/question/classes/statistics/questions/calculated.php @@ -57,7 +57,7 @@ class calculated { /** * @var string if this stat has been picked as a min, median or maximum facility value then this string says which stat this - * is. + * is. Prepended to question name for display. */ public $minmedianmaxnotice = ''; @@ -111,7 +111,6 @@ class calculated { */ public $randomguessscore = null; - // End of fields in db. protected $fieldsindb = array('questionid', 'slot', 'subquestion', 's', 'effectiveweight', 'negcovar', 'discriminationindex', @@ -179,6 +178,40 @@ class calculated { */ public $timemodified; + /** + * Set up a calculated instance ready to store a question's (or a variant of a slot's question's) + * stats for one slot in the quiz. + * + * @param null|object $question + * @param null|int $slot + * @param null|int $variant + */ + public function __construct($question = null, $slot = null, $variant = null) { + if ($question !== null) { + $this->questionid = $question->id; + $this->maxmark = $question->maxmark; + $this->positions = $question->number; + $this->question = $question; + } + if ($slot !== null) { + $this->slot = $slot; + } + if ($variant !== null) { + $this->variant = $variant; + } + } + + /** + * @return null|string a string that represents the pool of questions from this question draws if it random or null if not. + */ + public function random_selector_string() { + if ($this->question->qtype == 'random') { + return $this->question->category .'/'. $this->question->questiontext; + } else { + return null; + } + } + /** * Cache calculated stats stored in this object in 'question_statistics' table. * @@ -211,4 +244,18 @@ class calculated { $this->timemodified = $record->timemodified; } + public function sort_variants() { + ksort($this->variantstats); + } + + /** + * @return int[] array of sub-question ids or empty array if there are none. + */ + public function get_sub_question_ids() { + if ($this->subquestions !== '') { + return explode(',', $this->subquestions); + } else { + return array(); + } + } } diff --git a/question/classes/statistics/questions/calculated_for_subquestion.php b/question/classes/statistics/questions/calculated_for_subquestion.php index de740eba118..7229744a8f6 100644 --- a/question/classes/statistics/questions/calculated_for_subquestion.php +++ b/question/classes/statistics/questions/calculated_for_subquestion.php @@ -39,4 +39,17 @@ class calculated_for_subquestion extends calculated { * @var int only set immediately before display in the table. The order of display in the table. */ public $subqdisplayorder; + + /** + * @param object|null $step the step data for the step that this sub-question was first encountered in. + * @param int|null $variant the variant no + */ + public function __construct($step = null, $variant = null) { + if ($step !== null) { + $this->questionid = $step->questionid; + $this->maxmark = $step->maxmark; + } + $this->variant = $variant; + + } } diff --git a/question/classes/statistics/questions/calculator.php b/question/classes/statistics/questions/calculator.php index 940903f48b2..107024b60ed 100644 --- a/question/classes/statistics/questions/calculator.php +++ b/question/classes/statistics/questions/calculator.php @@ -37,20 +37,20 @@ defined('MOODLE_INTERNAL') || die(); class calculator { /** - * @var calculated[] + * @var all_calculated_for_qubaid_condition all the stats calculated for slots and sub-questions and variants of those + * questions. */ - public $questionstats = array(); - - /** - * @var calculated_for_subquestion[] - */ - public $subquestionstats = array(); + protected $stats; /** * @var float */ protected $sumofmarkvariance = 0; + /** + * @var array[] keyed by a string representing the pool of questions that this random question draws from. + * string as returned from {@link \core_question\statistics\questions\calculated::random_selector_string} + */ protected $randomselectors = array(); /** @@ -58,6 +58,8 @@ class calculator { */ protected $progress; + protected $statscollectionclassname = '\core_question\statistics\questions\all_calculated_for_qubaid_condition'; + /** * Constructor. * @@ -71,46 +73,16 @@ class calculator { $progress = new \core\progress\null(); } $this->progress = $progress; - + $this->stats = new $this->statscollectionclassname(); foreach ($questions as $slot => $question) { - $this->questionstats[$slot] = $this->new_slot_stats($question, $slot); + $this->stats->initialise_for_slot($slot, $question); + $this->stats->for_slot($slot)->randomguessscore = $this->get_random_guess_score($question); } } - /** - * Set up a calculated instance ready to store a questions stats. - * - * @param $question - * @param $slot - * @return calculated - */ - protected function new_slot_stats($question, $slot) { - $toreturn = new calculated(); - $toreturn->questionid = $question->id; - $toreturn->maxmark = $question->maxmark; - $toreturn->question = $question; - $toreturn->slot = $slot; - $toreturn->positions = $question->number; - $toreturn->randomguessscore = $this->get_random_guess_score($question); - return $toreturn; - } - - /** - * Set up a calculated instance ready to store a randomly selected question's stats. - * - * @param $step - * @return calculated_for_subquestion - */ - protected function new_subq_stats($step) { - $toreturn = new calculated_for_subquestion(); - $toreturn->questionid = $step->questionid; - $toreturn->maxmark = $step->maxmark; - return $toreturn; - } - /** * @param $qubaids \qubaid_condition - * @return array containing two arrays calculated[] and calculated_for_subquestion[]. + * @return all_calculated_for_qubaid_condition */ public function calculate($qubaids) { @@ -126,43 +98,40 @@ class calculator { $this->progress->increment_progress(); - $israndomquestion = ($step->questionid != $this->questionstats[$step->slot]->questionid); + $israndomquestion = ($step->questionid != $this->stats->for_slot($step->slot)->questionid); // If this is a variant we have not seen before create a place to store stats calculations for this variant. - if (!$israndomquestion && !isset($this->questionstats[$step->slot]->variantstats[$step->variant])) { - $this->questionstats[$step->slot]->variantstats[$step->variant] = - $this->new_slot_stats($this->questionstats[$step->slot]->question, $step->slot); - $this->questionstats[$step->slot]->variantstats[$step->variant]->variant = $step->variant; + if (!$israndomquestion && is_null($this->stats->for_slot($step->slot , $step->variant))) { + $this->stats->initialise_for_slot($step->slot, $this->stats->for_slot($step->slot)->question, $step->variant); + $this->stats->for_slot($step->slot, $step->variant)->randomguessscore = + $this->get_random_guess_score($this->stats->for_slot($step->slot)->question); } - // Step data walker for main question. - $this->initial_steps_walker($step, $this->questionstats[$step->slot], $summarks, true, !$israndomquestion); + $this->initial_steps_walker($step, $this->stats->for_slot($step->slot), $summarks, true, !$israndomquestion); // If this is a random question do the calculations for sub question stats. if ($israndomquestion) { - if (!isset($this->subquestionstats[$step->questionid])) { - $this->subquestionstats[$step->questionid] = $this->new_subq_stats($step); - } else if ($this->subquestionstats[$step->questionid]->maxmark != $step->maxmark) { - $this->subquestionstats[$step->questionid]->differentweights = true; + if (is_null($this->stats->for_subq($step->questionid))) { + $this->stats->initialise_for_subq($step); + } else if ($this->stats->for_subq($step->questionid)->maxmark != $step->maxmark) { + $this->stats->for_subq($step->questionid)->differentweights = true; } // If this is a variant of this subq we have not seen before create a place to store stats calculations for it. - if (!isset($this->subquestionstats[$step->questionid]->variantstats[$step->variant])) { - $this->subquestionstats[$step->questionid]->variantstats[$step->variant] = $this->new_subq_stats($step); - $this->subquestionstats[$step->questionid]->variantstats[$step->variant]->variant = $step->variant; + if (is_null($this->stats->for_subq($step->questionid, $step->variant))) { + $this->stats->initialise_for_subq($step, $step->variant); } - $this->initial_steps_walker($step, $this->subquestionstats[$step->questionid], $summarks, false); + $this->initial_steps_walker($step, $this->stats->for_subq($step->questionid), $summarks, false); // Extra stuff we need to do in this loop for subqs to keep track of where they need to be displayed later. - $number = $this->questionstats[$step->slot]->question->number; - $this->subquestionstats[$step->questionid]->usedin[$number] = $number; + $number = $this->stats->for_slot($step->slot)->question->number; + $this->stats->for_subq($step->questionid)->usedin[$number] = $number; // Keep track of which random questions are actually selected from each pool of questions that random // questions are pulled from. - $randomselectorstring = $this->questionstats[$step->slot]->question->category. '/' - .$this->questionstats[$step->slot]->question->questiontext; + $randomselectorstring = $this->stats->for_slot($step->slot)->random_selector_string(); if (!isset($this->randomselectors[$randomselectorstring])) { $this->randomselectors[$randomselectorstring] = array(); } @@ -173,67 +142,60 @@ class calculator { foreach ($this->randomselectors as $key => $notused) { ksort($this->randomselectors[$key]); + $this->randomselectors[$key] = implode(',', $this->randomselectors[$key]); } - $subquestions = question_load_questions(array_keys($this->subquestionstats)); + $this->stats->subquestions = question_load_questions($this->stats->get_all_subq_ids()); // Compute the statistics for sub questions, if there are any. - $this->progress->start_progress('', count($subquestions), 1); - foreach ($subquestions as $qid => $subquestion) { + $this->progress->start_progress('', count($this->stats->subquestions), 1); + foreach ($this->stats->subquestions as $qid => $subquestion) { $this->progress->increment_progress(); - $subquestion->maxmark = $this->subquestionstats[$qid]->maxmark; - $this->subquestionstats[$qid]->question = $subquestion; - $this->subquestionstats[$qid]->randomguessscore = $this->get_random_guess_score($subquestion); + $subquestion->maxmark = $this->stats->for_subq($qid)->maxmark; + $this->stats->for_subq($qid)->question = $subquestion; + $this->stats->for_subq($qid)->randomguessscore = $this->get_random_guess_score($subquestion); - foreach ($this->subquestionstats[$qid]->variantstats as $variantstat) { - $variantstat->question = $subquestion; - $variantstat->randomguessscore = $this->get_random_guess_score($subquestion); + $this->stats->for_subq($qid)->sort_variants(); + if ($variants = $this->stats->get_variants_for_subq($qid)) { + foreach ($variants as $variant) { + $this->stats->for_subq($qid, $variant)->question = $subquestion; + $this->stats->for_subq($qid, $variant)->randomguessscore = $this->get_random_guess_score($subquestion); + } } - $this->initial_question_walker($this->subquestionstats[$qid]); + $this->initial_question_walker($this->stats->for_subq($qid)); - if ($this->subquestionstats[$qid]->differentweights) { - // TODO output here really sucks, but throwing is too severe. - global $OUTPUT; - $name = $this->subquestionstats[$qid]->question->name; - echo $OUTPUT->notification( get_string('erroritemappearsmorethanoncewithdifferentweight', - 'quiz_statistics', $name)); - } - - if ($this->subquestionstats[$qid]->usedin) { - sort($this->subquestionstats[$qid]->usedin, SORT_NUMERIC); - $this->subquestionstats[$qid]->positions = implode(',', $this->subquestionstats[$qid]->usedin); + if ($this->stats->for_subq($qid)->usedin) { + sort($this->stats->for_subq($qid)->usedin, SORT_NUMERIC); + $this->stats->for_subq($qid)->positions = implode(',', $this->stats->for_subq($qid)->usedin); } else { - $this->subquestionstats[$qid]->positions = ''; + $this->stats->for_subq($qid)->positions = ''; } } $this->progress->end_progress(); - // Finish computing the averages, and put the subquestion data into the + // Finish computing the averages, and put the sub-question data into the // corresponding questions. // This cannot be a foreach loop because we need to have both // $question and $nextquestion available, but apart from that it is // foreach ($this->questions as $qid => $question). - reset($this->questionstats); - $this->progress->start_progress('', count($this->questionstats), 1); - while (list(, $questionstat) = each($this->questionstats)) { + $slots = $this->stats->get_all_slots(); + $this->progress->start_progress('', count($slots), 1); + while (list(, $slot) = each($slots)) { + $this->stats->for_slot($slot)->sort_variants(); $this->progress->increment_progress(); - $nextquestionstats = current($this->questionstats); + $nextslot = current($slots); - $this->initial_question_walker($questionstat); + $this->initial_question_walker($this->stats->for_slot($slot)); - // The rest of this loop is again to work out where randomly selected question stats should be displayed. - if ($questionstat->question->qtype == 'random') { - $randomselectorstring = $questionstat->question->category .'/'. $questionstat->question->questiontext; - if ($nextquestionstats && $nextquestionstats->question->qtype == 'random') { - $nextrandomselectorstring = - $nextquestionstats->question->category .'/'. $nextquestionstats->question->questiontext; - if ($randomselectorstring == $nextrandomselectorstring) { - continue; // Next loop iteration. - } + // The rest of this loop is to finish working out where randomly selected question stats should be displayed. + if ($this->stats->for_slot($slot)->question->qtype == 'random') { + $randomselectorstring = $this->stats->for_slot($slot)->random_selector_string(); + if ($nextslot && ($randomselectorstring == $this->stats->for_slot($nextslot)->random_selector_string())) { + continue; // Next loop iteration. } if (isset($this->randomselectors[$randomselectorstring])) { - $questionstat->subquestions = implode(',', $this->randomselectors[$randomselectorstring]); + $this->stats->for_slot($slot)->subquestions = $this->randomselectors[$randomselectorstring]; } } } @@ -243,122 +205,59 @@ class calculator { $this->progress->start_progress('', count($lateststeps), 1); foreach ($lateststeps as $step) { $this->progress->increment_progress(); - $israndomquestion = ($this->questionstats[$step->slot]->question->qtype == 'random'); - $this->secondary_steps_walker($step, $this->questionstats[$step->slot], $summarks, !$israndomquestion); + $israndomquestion = ($this->stats->for_slot($step->slot)->question->qtype == 'random'); + $this->secondary_steps_walker($step, $this->stats->for_slot($step->slot), $summarks, !$israndomquestion); - if ($this->questionstats[$step->slot]->subquestions) { - $this->secondary_steps_walker($step, $this->subquestionstats[$step->questionid], $summarks); + if ($this->stats->for_slot($step->slot)->subquestions) { + $this->secondary_steps_walker($step, $this->stats->for_subq($step->questionid), $summarks); } } $this->progress->end_progress(); - $this->progress->start_progress('', count($this->questionstats), 1); + $slots = $this->stats->get_all_slots(); + $this->progress->start_progress('', count($slots), 1); $sumofcovariancewithoverallmark = 0; - foreach ($this->questionstats as $questionstat) { + foreach ($this->stats->get_all_slots() as $slot) { $this->progress->increment_progress(); - $this->secondary_question_walker($questionstat); + $this->secondary_question_walker($this->stats->for_slot($slot)); - $this->sumofmarkvariance += $questionstat->markvariance; + $this->sumofmarkvariance += $this->stats->for_slot($slot)->markvariance; - if ($questionstat->covariancewithoverallmark >= 0) { - $sumofcovariancewithoverallmark += sqrt($questionstat->covariancewithoverallmark); + if ($this->stats->for_slot($slot)->covariancewithoverallmark >= 0) { + $sumofcovariancewithoverallmark += sqrt($this->stats->for_slot($slot)->covariancewithoverallmark); } } $this->progress->end_progress(); - $this->progress->start_progress('', count($this->subquestionstats), 1); - foreach ($this->subquestionstats as $subquestionstat) { + $subqids = $this->stats->get_all_subq_ids(); + $this->progress->start_progress('', count($subqids), 1); + foreach ($subqids as $subqid) { $this->progress->increment_progress(); - $this->secondary_question_walker($subquestionstat); + $this->secondary_question_walker($this->stats->for_subq($subqid)); } $this->progress->end_progress(); - foreach ($this->questionstats as $questionstat) { + foreach ($this->stats->get_all_slots() as $slot) { if ($sumofcovariancewithoverallmark) { - if ($questionstat->negcovar) { - $questionstat->effectiveweight = null; + if ($this->stats->for_slot($slot)->negcovar) { + $this->stats->for_slot($slot)->effectiveweight = null; } else { - $questionstat->effectiveweight = 100 * sqrt($questionstat->covariancewithoverallmark) / - $sumofcovariancewithoverallmark; + $this->stats->for_slot($slot)->effectiveweight = + 100 * sqrt($this->stats->for_slot($slot)->covariancewithoverallmark) / + $sumofcovariancewithoverallmark; } } else { - $questionstat->effectiveweight = null; + $this->stats->for_slot($slot)->effectiveweight = null; } } - $this->cache_stats($qubaids); + $this->stats->cache($qubaids); // All finished. $this->progress->end_progress(); } - return array($this->questionstats, $this->subquestionstats); + return $this->stats; } - /** - * Load cached statistics from the database. - * - * @param $qubaids \qubaid_condition - * @return array containing two arrays calculated[] and calculated_for_subquestion[]. - */ - public function get_cached($qubaids) { - global $DB; - $timemodified = time() - self::TIME_TO_CACHE; - $questionstatrecs = $DB->get_records_select('question_statistics', 'hashcode = ? AND timemodified > ?', - array($qubaids->get_hash_code(), $timemodified)); - - $questionids = array(); - foreach ($questionstatrecs as $fromdb) { - if (is_null($fromdb->variant) && !$fromdb->slot) { - $questionids[] = $fromdb->questionid; - } - } - $subquestions = question_load_questions($questionids); - foreach ($questionstatrecs as $fromdb) { - if (is_null($fromdb->variant)) { - if ($fromdb->slot) { - $this->questionstats[$fromdb->slot]->populate_from_record($fromdb); - // Array created in constructor and populated from question. - } else { - $this->subquestionstats[$fromdb->questionid] = new calculated_for_subquestion(); - $this->subquestionstats[$fromdb->questionid]->populate_from_record($fromdb); - $this->subquestionstats[$fromdb->questionid]->question = $subquestions[$fromdb->questionid]; - } - } - } - // Add cached variant stats to data structure. - foreach ($questionstatrecs as $fromdb) { - if (!is_null($fromdb->variant)) { - if ($fromdb->slot) { - $newcalcinstance = new calculated(); - $this->questionstats[$fromdb->slot]->variantstats[$fromdb->variant] = $newcalcinstance; - $newcalcinstance->question = $this->questionstats[$fromdb->slot]->question; - } else { - $newcalcinstance = new calculated_for_subquestion(); - $this->subquestionstats[$fromdb->questionid]->variantstats[$fromdb->variant] = $newcalcinstance; - $newcalcinstance->question = $subquestions[$fromdb->questionid]; - } - $newcalcinstance->populate_from_record($fromdb); - } - } - return array($this->questionstats, $this->subquestionstats); - } - - /** - * Find time of non-expired statistics in the database. - * - * @param $qubaids \qubaid_condition - * @return integer|boolean Time of cached record that matches this qubaid_condition or false is non found. - */ - public function get_last_calculated_time($qubaids) { - global $DB; - - $timemodified = time() - self::TIME_TO_CACHE; - return $DB->get_field_select('question_statistics', 'timemodified', 'hashcode = ? AND timemodified > ?', - array($qubaids->get_hash_code(), $timemodified), IGNORE_MULTIPLE); - } - - /** @var integer Time after which statistics are automatically recomputed. */ - const TIME_TO_CACHE = 900; // 15 minutes. - /** * Used when computing Coefficient of Internal Consistency by quiz statistics. * @@ -385,7 +284,7 @@ class calculator { qa.maxmark, qas.fraction * qa.maxmark as mark"; - $lateststeps = $dm->load_questions_usages_latest_steps($qubaids, array_keys($this->questionstats), $fields); + $lateststeps = $dm->load_questions_usages_latest_steps($qubaids, $this->stats->get_all_slots(), $fields); $summarks = array(); if ($lateststeps) { foreach ($lateststeps as $step) { @@ -536,7 +435,6 @@ class calculator { $stats->discriminativeefficiency = null; } - if ($dovariantsalso) { foreach ($stats->variantstats as $variantstat) { $this->secondary_question_walker($variantstat, false); @@ -554,16 +452,23 @@ class calculator { } /** + * Find time of non-expired statistics in the database. + * * @param $qubaids \qubaid_condition + * @return integer|boolean Time of cached record that matches this qubaid_condition or false is non found. */ - protected function cache_stats($qubaids) { - foreach ($this->questionstats as $questionstat) { - $questionstat->cache($qubaids); - } - - foreach ($this->subquestionstats as $subquestionstat) { - $subquestionstat->cache($qubaids); - } + public function get_last_calculated_time($qubaids) { + return $this->stats->get_last_calculated_time($qubaids); } + /** + * Load cached statistics from the database. + * + * @param $qubaids \qubaid_condition + * @return all_calculated_for_qubaid_condition + */ + public function get_cached($qubaids) { + $this->stats->get_cached($qubaids); + return $this->stats; + } }