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;
+ }
}