MDL-41754 (2) quiz statistics : using Sam's new progress classes
authorJames Pratt <me@jamiep.org>
Tue, 19 Nov 2013 13:07:05 +0000 (20:07 +0700)
committerJames Pratt <me@jamiep.org>
Mon, 27 Jan 2014 10:45:50 +0000 (17:45 +0700)
lib/classes/progress/display_if_slow.php
mod/quiz/report/statistics/classes/calculated.php
mod/quiz/report/statistics/classes/calculator.php
mod/quiz/report/statistics/lang/en/quiz_statistics.php
mod/quiz/report/statistics/report.php
mod/quiz/report/statistics/statistics_form.php
mod/quiz/report/statistics/tests/stats_from_steps_walkthrough_test.php
question/classes/statistics/questions/calculator.php

index c083300..7323f96 100644 (file)
@@ -61,10 +61,10 @@ class display_if_slow extends display {
      * Constructs the progress reporter. This will not output HTML just yet,
      * until the required delay time expires.
      *
-     * @param string $heading Text to display above bar (if it appears); '' for none
+     * @param string $heading Text to display above bar (if it appears); '' for none (default)
      * @param int $delay Delay time (default 5 seconds)
      */
-    public function __construct($heading, $delay = self::DEFAULT_DISPLAY_DELAY) {
+    public function __construct($heading = '', $delay = self::DEFAULT_DISPLAY_DELAY) {
         // Set start time based on delay.
         $this->starttime = time() + $delay;
         $this->heading = $heading;
index 3f5b6e6..223589b 100644 (file)
 // You should have received a copy of the GNU General Public License
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
+namespace quiz_statistics;
+
+defined('MOODLE_INTERNAL') || die();
+
 /**
  * The statistics calculator returns an instance of this class which contains the calculated statistics.
  *
@@ -26,7 +30,7 @@
  * @author     James Pratt me@jamiep.org
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class quiz_statistics_calculated {
+class calculated {
 
     /**
      * @param  string $whichattempts which attempts to use, represented internally as one of the constants as used in
@@ -110,7 +114,7 @@ class quiz_statistics_calculated {
      * @return int|float
      */
     protected function get_field($field) {
-        $fieldname = quiz_statistics_calculator::using_attempts_string_id($this->whichattempts).$field;
+        $fieldname = calculator::using_attempts_string_id($this->whichattempts).$field;
         return $this->{$fieldname};
     }
 
@@ -182,7 +186,7 @@ class quiz_statistics_calculated {
             }
 
             $quizinfo[get_string($property, 'quiz_statistics',
-                                 quiz_statistics_calculator::using_attempts_lang_string($this->whichattempts))] = $formattedvalue;
+                                 calculator::using_attempts_lang_string($this->whichattempts))] = $formattedvalue;
         }
 
         return $quizinfo;
@@ -199,12 +203,12 @@ class quiz_statistics_calculated {
     /**
      * Cache the stats contained in this class.
      *
-     * @param $qubaids qubaid_condition
+     * @param $qubaids \qubaid_condition
      */
     public function cache($qubaids) {
         global $DB;
 
-        $toinsert = new stdClass();
+        $toinsert = new \stdClass();
 
         foreach ($this->fieldsindb as $field) {
             $toinsert->{$field} = $this->{$field};
@@ -229,7 +233,7 @@ class quiz_statistics_calculated {
     /**
      * Given a record from 'quiz_statistics' table load the data into the properties of this class.
      *
-     * @param $record from db.
+     * @param $record \stdClass from db.
      */
     public function populate_from_record($record) {
         foreach ($this->fieldsindb as $field) {
index 3ad6dac..0e7d127 100644 (file)
@@ -14,6 +14,9 @@
 // You should have received a copy of the GNU General Public License
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
+namespace quiz_statistics;
+defined('MOODLE_INTERNAL') || die();
+
 /**
  * Class to calculate and also manage caching of quiz statistics.
  *
  * @author     James Pratt me@jamiep.org
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class quiz_statistics_calculator {
+class calculator {
+
+    /**
+     * @var \core\progress\base
+     */
+    protected $progress;
+
+    public function __construct(\core\progress\base $progress = null) {
+        if ($progress === null) {
+            $progress = new \core\progress\null();
+        }
+        $this->progress = $progress;
+    }
 
     /**
      * Compute the quiz statistics.
@@ -39,69 +54,70 @@ class quiz_statistics_calculator {
      * @param array $groupstudents     students in this group.
      * @param int   $p                 number of positions (slots).
      * @param float $sumofmarkvariance sum of mark variance, calculated as part of question statistics
-     * @return quiz_statistics_calculated $quizstats The statistics for overall attempt scores.
+     * @return calculated $quizstats The statistics for overall attempt scores.
      */
     public function calculate($quizid, $whichattempts, $groupstudents, $p, $sumofmarkvariance) {
 
+        $this->progress->start_progress('', 3);
 
-
-        $quizstats = new quiz_statistics_calculated($whichattempts);
+        $quizstats = new calculated($whichattempts);
 
         $countsandaverages = $this->attempt_counts_and_averages($quizid, $groupstudents);
+        $this->progress->progress(1);
 
         foreach ($countsandaverages as $propertyname => $value) {
             $quizstats->{$propertyname} = $value;
         }
 
         $s = $quizstats->s();
-
-        if ($s == 0) {
-            return $quizstats;
-        }
-
-        // Recalculate sql again this time possibly including test for first attempt.
-        list($fromqa, $whereqa, $qaparams) =
-            quiz_statistics_attempts_sql($quizid, $groupstudents, $whichattempts);
-
-        $quizstats->median = $this->median($s, $fromqa, $whereqa, $qaparams);
-
-        if ($s > 1) {
-
-            $powers = $this->sum_of_powers_of_difference_to_mean($quizstats->avg(), $fromqa, $whereqa, $qaparams);
-
-            $quizstats->standarddeviation = sqrt($powers->power2 / ($s - 1));
-
-            // Skewness.
-            if ($s > 2) {
-                // See http://docs.moodle.org/dev/Quiz_item_analysis_calculations_in_practise#Skewness_and_Kurtosis.
-                $m2 = $powers->power2 / $s;
-                $m3 = $powers->power3 / $s;
-                $m4 = $powers->power4 / $s;
-
-                $k2 = $s * $m2 / ($s - 1);
-                $k3 = $s * $s * $m3 / (($s - 1) * ($s - 2));
-                if ($k2 != 0) {
-                    $quizstats->skewness = $k3 / (pow($k2, 3 / 2));
-
-                    // Kurtosis.
-                    if ($s > 3) {
-                        $k4 = $s * $s * ((($s + 1) * $m4) - (3 * ($s - 1) * $m2 * $m2)) / (($s - 1) * ($s - 2) * ($s - 3));
-                        $quizstats->kurtosis = $k4 / ($k2 * $k2);
+        if ($s != 0) {
+
+            // Recalculate sql again this time possibly including test for first attempt.
+            list($fromqa, $whereqa, $qaparams) =
+                quiz_statistics_attempts_sql($quizid, $groupstudents, $whichattempts);
+
+            $quizstats->median = $this->median($s, $fromqa, $whereqa, $qaparams);
+            $this->progress->progress(2);
+
+            if ($s > 1) {
+
+                $powers = $this->sum_of_powers_of_difference_to_mean($quizstats->avg(), $fromqa, $whereqa, $qaparams);
+                $this->progress->progress(3);
+
+                $quizstats->standarddeviation = sqrt($powers->power2 / ($s - 1));
+
+                // Skewness.
+                if ($s > 2) {
+                    // See http://docs.moodle.org/dev/Quiz_item_analysis_calculations_in_practise#Skewness_and_Kurtosis.
+                    $m2 = $powers->power2 / $s;
+                    $m3 = $powers->power3 / $s;
+                    $m4 = $powers->power4 / $s;
+
+                    $k2 = $s * $m2 / ($s - 1);
+                    $k3 = $s * $s * $m3 / (($s - 1) * ($s - 2));
+                    if ($k2 != 0) {
+                        $quizstats->skewness = $k3 / (pow($k2, 3 / 2));
+
+                        // Kurtosis.
+                        if ($s > 3) {
+                            $k4 = $s * $s * ((($s + 1) * $m4) - (3 * ($s - 1) * $m2 * $m2)) / (($s - 1) * ($s - 2) * ($s - 3));
+                            $quizstats->kurtosis = $k4 / ($k2 * $k2);
+                        }
+
+                        if ($p > 1) {
+                            $quizstats->cic = (100 * $p / ($p - 1)) * (1 - ($sumofmarkvariance / $k2));
+                            $quizstats->errorratio = 100 * sqrt(1 - ($quizstats->cic / 100));
+                            $quizstats->standarderror = $quizstats->errorratio *
+                                $quizstats->standarddeviation / 100;
+                        }
                     }
 
-                    if ($p > 1) {
-                        $quizstats->cic = (100 * $p / ($p - 1)) * (1 - ($sumofmarkvariance / $k2));
-                        $quizstats->errorratio = 100 * sqrt(1 - ($quizstats->cic / 100));
-                        $quizstats->standarderror = $quizstats->errorratio *
-                            $quizstats->standarddeviation / 100;
-                    }
                 }
-
             }
-        }
-
-        $quizstats->cache(quiz_statistics_qubaids_condition($quizid, $groupstudents, $whichattempts));
 
+            $quizstats->cache(quiz_statistics_qubaids_condition($quizid, $groupstudents, $whichattempts));
+        }
+        $this->progress->end_progress();
         return $quizstats;
     }
 
@@ -111,8 +127,8 @@ class quiz_statistics_calculator {
     /**
      * Load cached statistics from the database.
      *
-     * @param $qubaids qubaid_condition
-     * @return quiz_statistics_calculated The statistics for overall attempt scores or false if not cached.
+     * @param $qubaids \qubaid_condition
+     * @return calculated The statistics for overall attempt scores or false if not cached.
      */
     public function get_cached($qubaids) {
         global $DB;
@@ -120,7 +136,7 @@ class quiz_statistics_calculator {
         $timemodified = time() - self::TIME_TO_CACHE;
         $fromdb = $DB->get_record_select('quiz_statistics', 'hashcode = ? AND timemodified > ?',
                                          array($qubaids->get_hash_code(), $timemodified));
-        $stats = new quiz_statistics_calculated();
+        $stats = new calculated();
         $stats->populate_from_record($fromdb);
         return $stats;
     }
@@ -128,7 +144,7 @@ class quiz_statistics_calculator {
     /**
      * Find time of non-expired statistics in the database.
      *
-     * @param $qubaids qubaid_condition
+     * @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) {
@@ -188,12 +204,12 @@ class quiz_statistics_calculator {
      *                                      #Calculating_MEAN_of_grades_for_all_attempts_by_students
      * @param int $quizid
      * @param array $groupstudents
-     * @return stdClass with properties with count and avg with prefixes firstattempts, highestattempts, etc.
+     * @return \stdClass with properties with count and avg with prefixes firstattempts, highestattempts, etc.
      */
     protected function attempt_counts_and_averages($quizid, $groupstudents) {
         global $DB;
 
-        $attempttotals = new stdClass();
+        $attempttotals = new \stdClass();
         foreach (array_keys(quiz_get_grading_options()) as $which) {
 
             list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql($quizid, $groupstudents, $which);
index 11ea4ee..0a18886 100644 (file)
@@ -33,6 +33,7 @@ $string['attemptsall'] = 'all attempts';
 $string['attemptsfirst'] = 'first attempt';
 $string['backtoquizreport'] = 'Back to main statistics report page.';
 $string['calculatefrom'] = 'Calculate statistics from';
+$string['calculatingallstats'] = 'Calculating statistics for quiz, questions and analysing repsonse data';
 $string['cic'] = 'Coefficient of internal consistency (for {$a})';
 $string['completestatsfilename'] = 'completestats';
 $string['count'] = 'Count';
index 8609335..4982223 100644 (file)
@@ -46,11 +46,16 @@ class quiz_statistics_report extends quiz_default_report {
     /** @var quiz_statistics_table instance of table class used for main questions stats table. */
     protected $table;
 
+    /** @var \core\progress\base|null $progress Handles progress reporting or not. */
+    protected $progress = null;
+
     /**
      * Display the report.
      */
     public function display($quiz, $cm, $course) {
-        global $CFG, $DB, $OUTPUT, $PAGE;
+        global $OUTPUT;
+
+        raise_memory_limit(MEMORY_HUGE);
 
         $this->context = context_module::instance($cm->id);
 
@@ -130,13 +135,19 @@ class quiz_statistics_report extends quiz_default_report {
                 get_string('quizstructureanalysis', 'quiz_statistics'));
         $questions = $this->load_and_initialise_questions_for_calculations($quiz);
 
+        // Print the page header stuff (if not downloading.
+        if (!$this->table->is_downloading()) {
+            $this->print_header_and_tabs($cm, $course, $quiz, 'statistics');
+        }
+
         if (!$nostudentsingroup) {
             // Get the data to be displayed.
+            $progress = $this->get_progress_trace_instance();
             list($quizstats, $questionstats, $subquestionstats) =
-                $this->get_quiz_and_questions_stats($quiz, $whichattempts, $groupstudents, $questions);
+                $this->get_all_stats_and_analysis($quiz, $whichattempts, $groupstudents, $questions, $progress);
         } else {
             // Or create empty stats containers.
-            $quizstats = new quiz_statistics_calculated($whichattempts);
+            $quizstats = new \quiz_statistics\calculated($whichattempts);
             $questionstats = array();
             $subquestionstats = array();
         }
@@ -146,9 +157,8 @@ class quiz_statistics_report extends quiz_default_report {
             $this->table->statistics_setup($quiz, $cm->id, $reporturl, $quizstats->s());
         }
 
-        // Print the page header stuff (if not downloading.
+        // Print the rest of the page header stuff (if not downloading.
         if (!$this->table->is_downloading()) {
-            $this->print_header_and_tabs($cm, $course, $quiz, 'statistics');
 
             if (groups_get_activity_groupmode($cm)) {
                 groups_print_activity_menu($cm, $reporturl->out());
@@ -488,36 +498,46 @@ class quiz_statistics_report extends quiz_default_report {
      * Get the quiz and question statistics, either by loading the cached results,
      * or by recomputing them.
      *
-     * @param object $quiz the quiz settings.
-     * @param string $whichattempts which attempts to use, represented internally as one of the constants as used in
+     * @param object $quiz               the quiz settings.
+     * @param string $whichattempts      which attempts to use, represented internally as one of the constants as used in
      *                                   $quiz->grademethod ie.
      *                                   QUIZ_GRADEAVERAGE, QUIZ_GRADEHIGHEST, QUIZ_ATTEMPTLAST or QUIZ_ATTEMPTFIRST
      *                                   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.
+     * @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.
      */
-    public function get_quiz_and_questions_stats($quiz, $whichattempts, $groupstudents, $questions) {
+    public function get_all_stats_and_analysis($quiz, $whichattempts, $groupstudents, $questions, $progress = null) {
+
+        if ($progress === null) {
+            $progress = new \core\progress\null();
+        }
 
         $qubaids = quiz_statistics_qubaids_condition($quiz->id, $groupstudents, $whichattempts);
 
-        $qcalc = new \core_question\statistics\questions\calculator($questions);
+        $qcalc = new \core_question\statistics\questions\calculator($questions, $progress);
 
-        $quizcalc = new quiz_statistics_calculator();
+        $quizcalc = new \quiz_statistics\calculator($progress);
 
         if ($quizcalc->get_last_calculated_time($qubaids) === false) {
+
+            $progress->start_progress('', 3);
+
             // Recalculate now.
             list($questionstats, $subquestionstats) = $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);
+                $this->analyse_responses_for_all_questions_and_subquestions($qubaids, $questions, $subquestionstats, $progress);
             }
+            $progress->progress(3);
+            $progress->end_progress();
         } else {
             $quizstats = $quizcalc->get_cached($qubaids);
             list($questionstats, $subquestionstats) = $qcalc->get_cached($qubaids);
@@ -526,10 +546,41 @@ class quiz_statistics_report extends quiz_default_report {
         return array($quizstats, $questionstats, $subquestionstats);
     }
 
-    protected function analyse_responses_for_all_questions_and_subquestions($qubaids, $questions, $subquestionstats) {
+    /**
+     * Appropriate instance depending if we want html output for the user or not.
+     *
+     * @return \core\progress\base child of \core\progress\base to handle the display (or not) of task progress.
+     */
+    protected function get_progress_trace_instance() {
+        if ($this->progress === null) {
+            if (!$this->table->is_downloading()) {
+                $this->progress =  new \core\progress\display_if_slow(get_string('calculatingallstats', 'quiz_statistics'));
+                $this->progress->set_display_names();
+            } else {
+                $this->progress = new \core\progress\null();
+            }
+        }
+        return $this->progress;
+    }
+
+    protected function analyse_responses_for_all_questions_and_subquestions($qubaids, $questions, $subquestionstats,
+                                                                            $progress = null) {
+
+        if ($progress === null) {
+            $progress = new \core\progress\null();
+        }
+
+        // Starting response analysis tasks.
+        $progress->start_progress('', count($questions) + count($subquestionstats));
+
+        // Starting response analysis of main questions.
+        $progress->start_progress('', count($questions), count($questions));
 
         $done = array();
+        $donecount = 1;
         foreach ($questions as $question) {
+            $progress->progress($donecount);
+            $donecount++;
             if (!question_bank::get_qtype($question->qtype, false)->can_analyse_responses()) {
                 continue;
             }
@@ -538,8 +589,15 @@ class quiz_statistics_report extends quiz_default_report {
             $responesstats = new \core_question\statistics\responses\analyser($question);
             $responesstats->calculate($qubaids);
         }
+        $progress->end_progress();
 
+        // Starting response analysis of sub-questions.
+        $countsubquestions = count($subquestionstats);
+        $progress->start_progress('', $countsubquestions, $countsubquestions);
+        $donecount = 1;
         foreach ($subquestionstats as $subquestionstat) {
+            $progress->progress($donecount);
+            $donecount++;
             if (!question_bank::get_qtype($subquestionstat->question->qtype, false)->can_analyse_responses() ||
                     isset($done[$subquestionstat->question->id])) {
                 continue;
@@ -549,6 +607,11 @@ class quiz_statistics_report extends quiz_default_report {
             $responesstats = new \core_question\statistics\responses\analyser($subquestionstat->question);
             $responesstats->calculate($qubaids);
         }
+        // Finished sub-question tasks.
+        $progress->end_progress();
+
+        // Finished all response analysis tasks.
+        $progress->end_progress();
     }
 
     /**
index 18071eb..114ae32 100644 (file)
@@ -40,7 +40,7 @@ class quiz_statistics_settings_form extends moodleform {
 
         $options = array();
         foreach (array_keys(quiz_get_grading_options()) as $which) {
-            $options[$which] = \quiz_statistics_calculator::using_attempts_lang_string($which);
+            $options[$which] = \quiz_statistics\calculator::using_attempts_lang_string($which);
         }
 
         $mform->addElement('select', 'whichattempts', get_string('calculatefrom', 'quiz_statistics'), $options);
index eb1be28..e712aab 100644 (file)
@@ -81,13 +81,13 @@ class quiz_report_statistics_from_steps_testcase extends mod_quiz_attempt_walkth
         $groupstudents = array();
         $questions = $this->report->load_and_initialise_questions_for_calculations($this->quiz);
         list($quizstats, $questionstats, $subquestionstats) =
-                        $this->report->get_quiz_and_questions_stats($this->quiz, $whichattempts, $groupstudents, $questions);
+                        $this->report->get_all_stats_and_analysis($this->quiz, $whichattempts, $groupstudents, $questions);
 
         $qubaids = quiz_statistics_qubaids_condition($this->quiz->id, $groupstudents, $whichattempts);
 
         // We will create some quiz and question stat calculator instances and some response analyser instances, just in order
         // to check the time of the
-        $quizcalc = new quiz_statistics_calculator();
+        $quizcalc = new \quiz_statistics\calculator();
         // Should not be a delay of more than one second between the calculation of stats above and here.
         $this->assertTimeCurrent($quizcalc->get_last_calculated_time($qubaids));
 
index a3e9d98..8fa4392 100644 (file)
@@ -53,13 +53,25 @@ class calculator {
 
     protected $randomselectors = array();
 
+    /**
+     * @var \progress_trace
+     */
+    protected $progress;
+
     /**
      * Constructor.
      *
      * @param object[] questions to analyze, keyed by slot, also analyses sub questions for random questions.
      *                              we expect some extra fields - slot, maxmark and number on the full question data objects.
+     * @param \core\progress\base|null $progress the element to send progress messages to, default is {@link \core\progress\null}.
      */
-    public function __construct($questions) {
+    public function __construct($questions, $progress = null) {
+
+        if ($progress === null) {
+            $progress = new \core\progress\null();
+        }
+        $this->progress = $progress;
+
         foreach ($questions as $slot => $question) {
             $this->questionstats[$slot] = new calculated();
             $this->questionstats[$slot]->questionid = $question->id;
@@ -76,15 +88,19 @@ class calculator {
      * @return array containing two arrays calculated[] and calculated_for_subquestion[].
      */
     public function calculate($qubaids) {
-        \core_php_time_limit::raise();
+
+        $this->progress->start_progress('', 6);
 
         list($lateststeps, $summarks) = $this->get_latest_steps($qubaids);
 
         if ($lateststeps) {
-
+            $this->progress->start_progress('', count($lateststeps), 1);
             // Compute the statistics of position, and for random questions, work
             // out which questions appear in which positions.
+            $countdone = 1;
             foreach ($lateststeps as $step) {
+                $this->progress->progress($countdone);
+                $countdone++;
                 $this->initial_steps_walker($step, $this->questionstats[$step->slot], $summarks);
 
                 // If this is a random question what is the real item being used?
@@ -110,6 +126,7 @@ class calculator {
                     $this->randomselectors[$randomselectorstring][$step->questionid] = $step->questionid;
                 }
             }
+            $this->progress->end_progress();
 
             foreach ($this->randomselectors as $key => $notused) {
                 ksort($this->randomselectors[$key]);
@@ -117,7 +134,11 @@ class calculator {
 
             // Compute the statistics of question id, if we need any.
             $subquestions = question_load_questions(array_keys($this->subquestionstats));
+            $this->progress->start_progress('', count($subquestions), 1);
+            $countdone = 1;
             foreach ($subquestions as $qid => $subquestion) {
+                $this->progress->progress($countdone);
+                $countdone++;
                 $this->subquestionstats[$qid]->question = $subquestion;
                 $this->subquestionstats[$qid]->question->maxmark = $this->subquestionstats[$qid]->maxmark;
                 $this->subquestionstats[$qid]->randomguessscore = $this->get_random_guess_score($subquestion);
@@ -139,6 +160,7 @@ class calculator {
                     $this->subquestionstats[$qid]->positions = '';
                 }
             }
+            $this->progress->end_progress();
 
             // Finish computing the averages, and put the subquestion data into the
             // corresponding questions.
@@ -147,7 +169,11 @@ class calculator {
             // $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);
+            $countdone = 1;
             while (list($slot, $questionstat) = each($this->questionstats)) {
+                $this->progress->progress($countdone);
+                $countdone++;
                 $nextquestionstats = current($this->questionstats);
 
                 $this->initial_question_walker($questionstat);
@@ -166,18 +192,28 @@ class calculator {
                     }
                 }
             }
+            $this->progress->end_progress();
 
             // Go through the records one more time.
+            $this->progress->start_progress('', count($lateststeps), 1);
+            $countdone = 1;
             foreach ($lateststeps as $step) {
+                $this->progress->progress($countdone);
+                $countdone++;
                 $this->secondary_steps_walker($step, $this->questionstats[$step->slot], $summarks);
 
                 if ($this->questionstats[$step->slot]->subquestions) {
                     $this->secondary_steps_walker($step, $this->subquestionstats[$step->questionid], $summarks);
                 }
             }
+            $this->progress->end_progress();
 
+            $this->progress->start_progress('', count($this->questionstats), 1);
             $sumofcovariancewithoverallmark = 0;
+            $countdone = 1;
             foreach ($this->questionstats as $questionstat) {
+                $this->progress->progress($countdone);
+                $countdone++;
                 $this->secondary_question_walker($questionstat);
 
                 $this->sumofmarkvariance += $questionstat->markvariance;
@@ -186,10 +222,16 @@ class calculator {
                     $sumofcovariancewithoverallmark += sqrt($questionstat->covariancewithoverallmark);
                 }
             }
+            $this->progress->end_progress();
 
+            $this->progress->start_progress('', count($this->subquestionstats), 1);
+            $countdone = 1;
             foreach ($this->subquestionstats as $subquestionstat) {
+                $this->progress->progress($countdone);
+                $countdone++;
                 $this->secondary_question_walker($subquestionstat);
             }
+            $this->progress->end_progress();
 
             foreach ($this->questionstats as $questionstat) {
                 if ($sumofcovariancewithoverallmark) {
@@ -204,6 +246,9 @@ class calculator {
                 }
             }
             $this->cache_stats($qubaids);
+
+            // All finished.
+            $this->progress->end_progress();
         }
         return array($this->questionstats, $this->subquestionstats);
     }