MDL-41761 statistics should be calculable for last or highest attempts
authorJamie Pratt <me@jamiep.org>
Mon, 30 Sep 2013 11:05:46 +0000 (18:05 +0700)
committerJamie Pratt <me@jamiep.org>
Mon, 30 Sep 2013 11:05:46 +0000 (18:05 +0700)
as well as for first or all attempts.

This commit drops a database table and then recreates it. This is in
order to be able to have some new columns in the table be NOT NULL with
no default value, which would not be possible if I added fields to an
existing table.

The quiz_statistics table is used for caching calculated values only
and thus we can safely drop it and recreate it.

13 files changed:
mod/quiz/report/attemptsreport.php
mod/quiz/report/reportlib.php
mod/quiz/report/statistics/classes/calculated.php
mod/quiz/report/statistics/classes/calculator.php
mod/quiz/report/statistics/db/install.xml
mod/quiz/report/statistics/db/upgrade.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/statistics_graph.php
mod/quiz/report/statistics/statisticslib.php
mod/quiz/report/statistics/tests/stats_from_steps_walkthrough_test.php
mod/quiz/report/statistics/version.php

index fe223b7..4ea24a9 100644 (file)
@@ -84,8 +84,7 @@ abstract class quiz_attempts_report extends quiz_default_report {
         $this->qmsubselect = quiz_report_qm_filter_select($quiz);
 
         $this->form = new $formclass($this->get_base_url(),
-                array('qmsubselect' => $this->qmsubselect, 'quiz' => $quiz,
-                'currentgroup' => $currentgroup, 'context' => $this->context));
+                array('quiz' => $quiz, 'currentgroup' => $currentgroup, 'context' => $this->context));
 
         return array($currentgroup, $students, $groupstudents, $allowed);
     }
@@ -103,7 +102,7 @@ abstract class quiz_attempts_report extends quiz_default_report {
      * Get information about which students to show in the report.
      * @param object $cm the coures module.
      * @param object $course the course settings.
-     * @return an array with four elements:
+     * @return array with four elements:
      *      0 => integer the current group id (0 for none).
      *      1 => array ids of all the students in this course.
      *      2 => array ids of all the students in the current group.
index bc88dc8..a25a768 100644 (file)
@@ -137,52 +137,64 @@ function quiz_report_can_filter_only_graded($quiz) {
 }
 
 /**
- * Given the quiz grading method return sub select sql to find the id of the
- * one attempt that will be graded for each user. Or return
- * empty string if all attempts contribute to final grade.
+ * This is a wrapper for {@link quiz_report_grade_method_sql} that takes the whole quiz object instead of just the grading method
+ * as a param. See definition for {@link quiz_report_grade_method_sql} below.
+ *
+ * @param object $quiz
+ * @param string $quizattemptsalias sql alias for 'quiz_attempts' table
+ * @return string sql to test if this is an attempt that will contribute towards the grade of the user
  */
 function quiz_report_qm_filter_select($quiz, $quizattemptsalias = 'quiza') {
     if ($quiz->attempts == 1) {
         // This quiz only allows one attempt.
         return '';
     }
+    return quiz_report_grade_method_sql($quiz->grademethod, $quizattemptsalias);
+}
 
-    switch ($quiz->grademethod) {
+/**
+ * Given a quiz grading method return sql to test if this is an
+ * attempt that will be contribute towards the grade of the user. Or return an
+ * empty string if the grading method is QUIZ_GRADEAVERAGE and thus all attempts
+ * contribute to final grade.
+ *
+ * @param string $grademethod quiz grading method.
+ * @param string $quizattemptsalias sql alias for 'quiz_attempts' table
+ * @return string sql to test if this is an attempt that will contribute towards the graded of the user
+ */
+function quiz_report_grade_method_sql($grademethod, $quizattemptsalias = 'quiza') {
+    switch ($grademethod) {
         case QUIZ_GRADEHIGHEST :
             return "$quizattemptsalias.id = (
-                    SELECT MIN(qa2.id)
-                    FROM {quiz_attempts} qa2
-                    WHERE qa2.quiz = $quizattemptsalias.quiz AND
-                        qa2.userid = $quizattemptsalias.userid AND
-                        COALESCE(qa2.sumgrades, 0) = (
-                            SELECT MAX(COALESCE(qa3.sumgrades, 0))
-                            FROM {quiz_attempts} qa3
-                            WHERE qa3.quiz = $quizattemptsalias.quiz AND
-                                qa3.userid = $quizattemptsalias.userid
-                        )
-                    )";
+                            SELECT MIN(qa2.id)
+                            FROM {quiz_attempts} qa2
+                            WHERE qa2.quiz = $quizattemptsalias.quiz AND
+                                qa2.userid = $quizattemptsalias.userid AND
+                                COALESCE(qa2.sumgrades, 0) = (
+                                    SELECT MAX(COALESCE(qa3.sumgrades, 0))
+                                    FROM {quiz_attempts} qa3
+                                    WHERE qa3.quiz = $quizattemptsalias.quiz AND
+                                        qa3.userid = $quizattemptsalias.userid
+                                )
+                            )";
 
         case QUIZ_GRADEAVERAGE :
             return '';
 
         case QUIZ_ATTEMPTFIRST :
-            return "$quizattemptsalias.id = (
-                    SELECT MIN(qa2.id)
-                    FROM {quiz_attempts} qa2
-                    WHERE qa2.quiz = $quizattemptsalias.quiz AND
-                        qa2.userid = $quizattemptsalias.userid)";
+            return "$quizattemptsalias.attempt = 1";
 
         case QUIZ_ATTEMPTLAST :
             return "$quizattemptsalias.id = (
-                    SELECT MAX(qa2.id)
-                    FROM {quiz_attempts} qa2
-                    WHERE qa2.quiz = $quizattemptsalias.quiz AND
-                        qa2.userid = $quizattemptsalias.userid)";
+                            SELECT MAX(qa2.id)
+                            FROM {quiz_attempts} qa2
+                            WHERE qa2.quiz = $quizattemptsalias.quiz AND
+                                qa2.userid = $quizattemptsalias.userid)";
     }
 }
 
 /**
- * Get the nuber of students whose score was in a particular band for this quiz.
+ * Get the number of students whose score was in a particular band for this quiz.
  * @param number $bandwidth the width of each band.
  * @param int $bands the number of bands
  * @param int $quizid the quiz id.
@@ -225,10 +237,10 @@ ORDER BY
     $data = $DB->get_records_sql_menu($sql, $params);
 
     // We need to create array elements with values 0 at indexes where there is no element.
-    $data =  $data + array_fill(0, $bands + 1, 0);
+    $data = $data + array_fill(0, $bands + 1, 0);
     ksort($data);
 
-    // Place the maximum (prefect grade) into the last band i.e. make last
+    // Place the maximum (perfect grade) into the last band i.e. make last
     // band for example 9 <= g <=10 (where 10 is the perfect grade) rather than
     // just 9 <= g <10.
     $data[$bands - 1] += $data[$bands];
index 8103b7c..3f5b6e6 100644 (file)
  */
 class quiz_statistics_calculated {
 
-    public function __construct($allattempts = null) {
-        if ($allattempts !== null) {
-            $this->allattempts = $allattempts;
+    /**
+     * @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,
+     *                                   the default null value is used when constructing an instance whose values will be
+     *                                   populated from a db record.
+     */
+    public function __construct($whichattempts = null) {
+        if ($whichattempts !== null) {
+            $this->whichattempts = $whichattempts;
         }
     }
 
     /**
-     * @var bool whether we are calculating calculate stats from all attempts.
+     * @var int which attempts we are calculating calculate stats from.
      */
-    public $allattempts;
+    public $whichattempts;
 
     /* Following stats all described here : http://docs.moodle.org/dev/Quiz_statistics_calculations#Test_statistics  */
 
@@ -45,10 +53,18 @@ class quiz_statistics_calculated {
 
     public $allattemptscount = 0;
 
+    public $lastattemptscount = 0;
+
+    public $highestattemptscount = 0;
+
     public $firstattemptsavg;
 
     public $allattemptsavg;
 
+    public $lastattemptsavg;
+
+    public $highestattemptsavg;
+
     public $median;
 
     public $standarddeviation;
@@ -68,20 +84,34 @@ class quiz_statistics_calculated {
      */
     public $timemodified;
 
+    /**
+     * Count of attempts selected by $this->whichattempts
+     *
+     * @return int
+     */
     public function s() {
-        if ($this->allattempts) {
-            return $this->allattemptscount;
-        } else {
-            return $this->firstattemptscount;
-        }
+        return $this->get_field('count');
     }
 
+    /**
+     * Average grade for the attempts selected by $this->whichattempts
+     *
+     * @return float
+     */
     public function avg() {
-        if ($this->allattempts) {
-            return $this->allattemptsavg;
-        } else {
-            return $this->firstattemptsavg;
-        }
+        return $this->get_field('avg');
+    }
+
+    /**
+     * Get the right field name to fetch a stat for these attempts that is calculated for more than one $whichattempts (count or
+     * avg).
+     *
+     * @param string $field name of field
+     * @return int|float
+     */
+    protected function get_field($field) {
+        $fieldname = quiz_statistics_calculator::using_attempts_string_id($this->whichattempts).$field;
+        return $this->{$fieldname};
     }
 
     /**
@@ -97,6 +127,8 @@ class quiz_statistics_calculated {
                            'allattemptscount' => 'number',
                            'firstattemptsavg' => 'summarks_as_percentage',
                            'allattemptsavg' => 'summarks_as_percentage',
+                           'lastattemptsavg' => 'summarks_as_percentage',
+                           'highestattemptsavg' => 'summarks_as_percentage',
                            'median' => 'summarks_as_percentage',
                            'standarddeviation' => 'summarks_as_percentage',
                            'skewness' => 'number_format',
@@ -149,27 +181,18 @@ class quiz_statistics_calculated {
                     $formattedvalue = $value;
             }
 
-            $quizinfo[get_string($property, 'quiz_statistics', $this->using_attempts_string())] = $formattedvalue;
+            $quizinfo[get_string($property, 'quiz_statistics',
+                                 quiz_statistics_calculator::using_attempts_lang_string($this->whichattempts))] = $formattedvalue;
         }
 
         return $quizinfo;
     }
 
-    /**
-     * @return string the appropriate lang string to describe this option.
-     */
-    protected function using_attempts_string() {
-        if ($this->allattempts) {
-            return get_string('allattempts', 'quiz_statistics');
-        } else {
-            return get_string('firstattempts', 'quiz_statistics');
-        }
-    }
-
     /**
      * @var array of names of properties of this class that are cached in db record.
      */
-    protected $fieldsindb = array('allattempts', 'firstattemptscount', 'allattemptscount', 'firstattemptsavg', 'allattemptsavg',
+    protected $fieldsindb = array('whichattempts', 'firstattemptscount', 'allattemptscount', 'firstattemptsavg', 'allattemptsavg',
+                                    'lastattemptscount', 'highestattemptscount', 'lastattemptsavg', 'highestattemptsavg',
                                     'median', 'standarddeviation', 'skewness',
                                     'kurtosis', 'cic', 'errorratio', 'standarderror');
 
index 85c8cb7..dc77507 100644 (file)
@@ -32,16 +32,26 @@ class quiz_statistics_calculator {
      * Compute the quiz statistics.
      *
      * @param int   $quizid            the quiz id.
-     * @param int   $currentgroup      the current group. 0 for none.
-     * @param bool  $useallattempts    use all attempts, or just first attempts.
+     * @param int $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 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.
      */
-    public function calculate($quizid, $currentgroup, $useallattempts, $groupstudents, $p, $sumofmarkvariance) {
+    public function calculate($quizid, $whichattempts, $groupstudents, $p, $sumofmarkvariance) {
 
-        $quizstats = $this->attempt_counts_and_averages($quizid, $currentgroup, $useallattempts, $groupstudents);
+
+
+        $quizstats = new quiz_statistics_calculated($whichattempts);
+
+        $countsandaverages = $this->attempt_counts_and_averages($quizid, $groupstudents);
+
+        foreach ($countsandaverages as $propertyname => $value) {
+            $quizstats->{$propertyname} = $value;
+        }
 
         $s = $quizstats->s();
 
@@ -51,7 +61,7 @@ class quiz_statistics_calculator {
 
         // Recalculate sql again this time possibly including test for first attempt.
         list($fromqa, $whereqa, $qaparams) =
-            quiz_statistics_attempts_sql($quizid, $currentgroup, $groupstudents, $useallattempts);
+            quiz_statistics_attempts_sql($quizid, $groupstudents, $whichattempts);
 
         $quizstats->median = $this->median($s, $fromqa, $whereqa, $qaparams);
 
@@ -80,7 +90,7 @@ class quiz_statistics_calculator {
                     }
 
                     if ($p > 1) {
-                        $quizstats->cic = (100 * $p / ($p -1)) * (1 - ($sumofmarkvariance / $k2));
+                        $quizstats->cic = (100 * $p / ($p - 1)) * (1 - ($sumofmarkvariance / $k2));
                         $quizstats->errorratio = 100 * sqrt(1 - ($quizstats->cic / 100));
                         $quizstats->standarderror = $quizstats->errorratio *
                             $quizstats->standarddeviation / 100;
@@ -90,7 +100,7 @@ class quiz_statistics_calculator {
             }
         }
 
-        $quizstats->cache(quiz_statistics_qubaids_condition($quizid, $currentgroup, $groupstudents, $useallattempts));
+        $quizstats->cache(quiz_statistics_qubaids_condition($quizid, $groupstudents, $whichattempts));
 
         return $quizstats;
     }
@@ -129,61 +139,88 @@ class quiz_statistics_calculator {
                                          array($qubaids->get_hash_code(), $timemodified));
     }
 
+    /**
+     * Given a particular quiz grading method return a lang string describing which attempts contribute to grade.
+     *
+     * Note internally we use the grading method constants to represent which attempts we are calculating statistics for, each
+     * grading method corresponds to different attempts for each user.
+     *
+     * @param  int $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.
+     * @return string the appropriate lang string to describe this option.
+     */
+    public static function using_attempts_lang_string($whichattempts) {
+         return get_string(static::using_attempts_string_id($whichattempts), 'quiz_statistics');
+    }
+
+    /**
+     * Given a particular quiz grading method return a string id for use as a field name prefix in mdl_quiz_statistics or to
+     * fetch the appropriate language string describing which attempts contribute to grade.
+     *
+     * Note internally we use the grading method constants to represent which attempts we are calculating statistics for, each
+     * grading method corresponds to different attempts for each user.
+     *
+     * @param  int $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.
+     * @return string the string id for this option.
+     */
+    public static function using_attempts_string_id($whichattempts) {
+        switch ($whichattempts) {
+            case QUIZ_ATTEMPTFIRST :
+                return 'firstattempts';
+            case QUIZ_GRADEHIGHEST :
+                return 'highestattempts';
+            case QUIZ_ATTEMPTLAST :
+                return 'lastattempts';
+            case QUIZ_GRADEAVERAGE :
+                return 'allattempts';
+        }
+    }
+
     /**
      * Calculating count and mean of marks for first and ALL attempts by students.
      *
      * See : http://docs.moodle.org/dev/Quiz_item_analysis_calculations_in_practise
      *                                      #Calculating_MEAN_of_grades_for_all_attempts_by_students
      * @param int $quizid
-     * @param int $currentgroup
-     * @param bool $useallattempts
      * @param array $groupstudents
-     * @return quiz_statistics_calculated containing calculated counts, totals and averages.
+     * @return stdClass with properties with count and avg with prefixes firstattempts, highestattempts, etc.
      */
-    protected function attempt_counts_and_averages($quizid, $currentgroup, $useallattempts, $groupstudents) {
+    protected function attempt_counts_and_averages($quizid, $groupstudents) {
         global $DB;
 
-        $quizstats = new quiz_statistics_calculated($useallattempts);
-
-        list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql($quizid, $currentgroup, $groupstudents, true);
-
-        $attempttotals = $DB->get_records_sql("
-                SELECT
-                    CASE WHEN attempt = 1 THEN 1 ELSE 0 END AS isfirst,
-                    COUNT(1) AS countrecs,
-                    SUM(sumgrades) AS total
-                FROM $fromqa
-                WHERE $whereqa
-                GROUP BY CASE WHEN attempt = 1 THEN 1 ELSE 0 END", $qaparams);
-
-        // Above query that returns sums and counts for first attempt and other non first attempts.
-        // We want to work out stats for first attempt or ALL attempts.
-
-        if (isset($attempttotals[1])) {
-            $quizstats->firstattemptscount = $attempttotals[1]->countrecs;
-            $firstattemptstotal = $attempttotals[1]->total;
-        } else {
-            $quizstats->firstattemptscount = 0;
-            $firstattemptstotal = 0;
-        }
+        list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql($quizid, $groupstudents);
 
-        if (isset($attempttotals[0])) {
-            $quizstats->allattemptscount = $quizstats->firstattemptscount + $attempttotals[0]->countrecs;
-            $allattemptstotal = $firstattemptstotal + $attempttotals[0]->total;
-        } else {
-            $quizstats->allattemptscount = $quizstats->firstattemptscount;
-            $allattemptstotal = $firstattemptstotal;
+        $selects = array();
+        foreach (array_keys(quiz_get_grading_options()) as $which) {
+            $fieldprefix = static::using_attempts_string_id($which);
+            $condition = quiz_report_grade_method_sql($which);
+            if ($condition == '') {
+                $case = '1';
+            } else {
+                $case = "CASE WHEN ($condition) THEN 1 ELSE 0 END";
+            }
+            $selects[] = "
+                    SUM($case) AS {$fieldprefix}count,
+                    SUM(sumgrades * $case) AS {$fieldprefix}total";
         }
+        $select = join(',', $selects);
 
-        if ($quizstats->allattemptscount !== 0) {
-            $quizstats->allattemptsavg = $allattemptstotal / $quizstats->allattemptscount;
-        }
+        $attempttotals = $DB->get_record_sql("
+                SELECT {$select}
+                FROM $fromqa
+                WHERE $whereqa", $qaparams);
 
-        if ($quizstats->firstattemptscount !== 0) {
-            $quizstats->firstattemptsavg = $firstattemptstotal / $quizstats->firstattemptscount;
+        foreach (array_keys(quiz_get_grading_options()) as $which) {
+            $fieldprefix = static::using_attempts_string_id($which);
+            $attempttotals->{$fieldprefix.'avg'} = $attempttotals->{$fieldprefix.'total'} / $attempttotals->{$fieldprefix.'count'};
+            unset($attempttotals->{$fieldprefix.'total'});
         }
-
-        return $quizstats;
+        return $attempttotals;
     }
 
     /**
index 853270d..b53f0e1 100644 (file)
@@ -8,11 +8,15 @@
       <FIELDS>
         <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
         <FIELD NAME="hashcode" TYPE="char" LENGTH="40" NOTNULL="true" SEQUENCE="false" COMMENT="sha1 hash of serialized qubaids_condition class. Unique for every combination of class name and property."/>
-        <FIELD NAME="allattempts" TYPE="int" LENGTH="4" NOTNULL="true" SEQUENCE="false" COMMENT="bool used to indicate whether these stats are for all attempts or just for the first."/>
+        <FIELD NAME="whichattempts" TYPE="int" LENGTH="4" NOTNULL="true" SEQUENCE="false" COMMENT="bool used to indicate whether these stats are for all attempts or just for the first."/>
         <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="firstattemptscount" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="highestattemptscount" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="lastattemptscount" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="allattemptscount" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="firstattemptsavg" TYPE="number" LENGTH="15" NOTNULL="false" SEQUENCE="false" DECIMALS="5"/>
+        <FIELD NAME="highestattemptsavg" TYPE="number" LENGTH="15" NOTNULL="false" SEQUENCE="false" DECIMALS="5"/>
+        <FIELD NAME="lastattemptsavg" TYPE="number" LENGTH="15" NOTNULL="false" SEQUENCE="false" DECIMALS="5"/>
         <FIELD NAME="allattemptsavg" TYPE="number" LENGTH="15" NOTNULL="false" SEQUENCE="false" DECIMALS="5"/>
         <FIELD NAME="median" TYPE="number" LENGTH="15" NOTNULL="false" SEQUENCE="false" DECIMALS="5"/>
         <FIELD NAME="standarddeviation" TYPE="number" LENGTH="15" NOTNULL="false" SEQUENCE="false" DECIMALS="5"/>
index 3581dfe..9f98c14 100644 (file)
@@ -85,6 +85,51 @@ function xmldb_quiz_statistics_upgrade($oldversion) {
         upgrade_plugin_savepoint(true, 2013092000, 'quiz', 'statistics');
     }
 
+    if ($oldversion < 2013093000) {
+        // Define table quiz_statistics to be dropped.
+        $table = new xmldb_table('quiz_statistics');
+
+        // Conditionally launch drop table for quiz_statistics.
+        if ($dbman->table_exists($table)) {
+            $dbman->drop_table($table);
+        }
+
+        // Define table quiz_statistics to be created.
+        $table = new xmldb_table('quiz_statistics');
+
+        // Adding fields to table quiz_statistics.
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+        $table->add_field('hashcode', XMLDB_TYPE_CHAR, '40', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('whichattempts', XMLDB_TYPE_INTEGER, '4', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('firstattemptscount', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('highestattemptscount', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('lastattemptscount', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('allattemptscount', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('firstattemptsavg', XMLDB_TYPE_NUMBER, '15, 5', null, null, null, null);
+        $table->add_field('highestattemptsavg', XMLDB_TYPE_NUMBER, '15, 5', null, null, null, null);
+        $table->add_field('lastattemptsavg', XMLDB_TYPE_NUMBER, '15, 5', null, null, null, null);
+        $table->add_field('allattemptsavg', XMLDB_TYPE_NUMBER, '15, 5', null, null, null, null);
+        $table->add_field('median', XMLDB_TYPE_NUMBER, '15, 5', null, null, null, null);
+        $table->add_field('standarddeviation', XMLDB_TYPE_NUMBER, '15, 5', null, null, null, null);
+        $table->add_field('skewness', XMLDB_TYPE_NUMBER, '15, 10', null, null, null, null);
+        $table->add_field('kurtosis', XMLDB_TYPE_NUMBER, '15, 5', null, null, null, null);
+        $table->add_field('cic', XMLDB_TYPE_NUMBER, '15, 10', null, null, null, null);
+        $table->add_field('errorratio', XMLDB_TYPE_NUMBER, '15, 10', null, null, null, null);
+        $table->add_field('standarderror', XMLDB_TYPE_NUMBER, '15, 10', null, null, null, null);
+
+        // Adding keys to table quiz_statistics.
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+
+        // Conditionally launch create table for quiz_statistics.
+        if (!$dbman->table_exists($table)) {
+            $dbman->create_table($table);
+        }
+
+        // Statistics savepoint reached.
+        upgrade_plugin_savepoint(true, 2013093000, 'quiz', 'statistics');
+    }
+
     return true;
 }
 
index a83cfa3..11ea4ee 100644 (file)
@@ -56,8 +56,12 @@ $string['firstattempts'] = 'first attempts';
 $string['firstattemptsavg'] = 'Average grade of first attempts';
 $string['firstattemptscount'] = 'Number of complete graded first attempts';
 $string['frequency'] = 'Frequency';
+$string['highestattempts'] = 'highest graded attempt';
+$string['highestattemptsavg'] = 'Average grade of highest graded attempts';
 $string['intended_weight'] = 'Intended weight';
 $string['kurtosis'] = 'Score distribution kurtosis (for {$a})';
+$string['lastattempts'] = 'last attempt';
+$string['lastattemptsavg'] = 'Average grade of last attempts';
 $string['lastcalculated'] = 'Last calculated {$a->lastcalculated} ago there have been {$a->count} attempts since then.';
 $string['median'] = 'Median grade (for {$a})';
 $string['modelresponse'] = 'Model response';
@@ -85,6 +89,7 @@ $string['quizoverallstatistics'] = 'Quiz overall statistics';
 $string['quizstructureanalysis'] = 'Quiz structure analysis';
 $string['random_guess_score'] = 'Random guess score';
 $string['recalculatenow'] = 'Recalculate now';
+$string['reportsettings'] = 'Statistics calculation settings';
 $string['response'] = 'Response';
 $string['skewness'] = 'Score distribution skewness (for {$a})';
 $string['standarddeviation'] = 'Standard deviation (for {$a})';
index 19fa679..3cc1e2a 100644 (file)
@@ -61,6 +61,7 @@ class quiz_statistics_report extends quiz_default_report {
         // A qid paramter indicates we should display the detailed analysis of a sub question.
         $qid = optional_param('qid', 0, PARAM_INT);
         $slot = optional_param('slot', 0, PARAM_INT);
+        $whichattempts = optional_param('whichattempts', $quiz->grademethod, PARAM_INT);
 
         $pageoptions = array();
         $pageoptions['id'] = $cm->id;
@@ -69,17 +70,15 @@ class quiz_statistics_report extends quiz_default_report {
         $reporturl = new moodle_url('/mod/quiz/report.php', $pageoptions);
 
         $mform = new quiz_statistics_settings_form($reporturl);
+
+        $mform->set_data(array('whichattempts' => $whichattempts));
+
         if ($fromform = $mform->get_data()) {
-            $useallattempts = $fromform->useallattempts;
-            if ($fromform->useallattempts) {
-                set_user_preference('quiz_report_statistics_useallattempts',
-                        $fromform->useallattempts);
-            } else {
-                unset_user_preference('quiz_report_statistics_useallattempts');
-            }
+            $whichattempts = $fromform->whichattempts;
+        }
 
-        } else {
-            $useallattempts = get_user_preferences('quiz_report_statistics_useallattempts', 0);
+        if ($whichattempts != $quiz->grademethod) {
+            $reporturl->param('whichattempts', $whichattempts);
         }
 
         // Find out current groups mode.
@@ -103,7 +102,7 @@ class quiz_statistics_report extends quiz_default_report {
             }
         }
 
-        $qubaids = quiz_statistics_qubaids_condition($quiz->id, $currentgroup, $groupstudents, $useallattempts);
+        $qubaids = quiz_statistics_qubaids_condition($quiz->id, $groupstudents, $whichattempts);
 
         // If recalculate was requested, handle that.
         if ($recalculate && confirm_sesskey()) {
@@ -128,10 +127,10 @@ class quiz_statistics_report extends quiz_default_report {
         if (!$nostudentsingroup) {
             // Get the data to be displayed.
             list($quizstats, $questionstats, $subquestionstats) =
-                $this->get_quiz_and_questions_stats($quiz, $currentgroup, $useallattempts, $groupstudents, $questions);
+                $this->get_quiz_and_questions_stats($quiz, $whichattempts, $groupstudents, $questions);
         } else {
             // Or create empty stats containers.
-            $quizstats = new quiz_statistics_calculated($useallattempts);
+            $quizstats = new quiz_statistics_calculated($whichattempts);
             $questionstats = array();
             $subquestionstats = array();
         }
@@ -159,7 +158,6 @@ class quiz_statistics_report extends quiz_default_report {
             }
 
             // Print display options form.
-            $mform->set_data(array('useallattempts' => $useallattempts));
             $mform->display();
         }
 
@@ -172,7 +170,7 @@ class quiz_statistics_report extends quiz_default_report {
                 $this->output_quiz_structure_analysis_table($quizstats->s(), $questionstats, $subquestionstats);
 
                 if ($this->table->is_downloading() == 'xhtml' && $quizstats->s() != 0) {
-                    $this->output_statistics_graph($quiz->id, $currentgroup, $useallattempts);
+                    $this->output_statistics_graph($quiz->id, $currentgroup, $whichattempts);
                 }
 
                 foreach ($questions as $slot => $question) {
@@ -232,15 +230,14 @@ class quiz_statistics_report extends quiz_default_report {
         } else {
             // On-screen display of overview report.
             echo $OUTPUT->heading(get_string('quizinformation', 'quiz_statistics'));
-            echo $this->output_caching_info($quizstats, $quiz->id, $currentgroup,
-                    $groupstudents, $useallattempts, $reporturl);
+            echo $this->output_caching_info($quizstats, $quiz->id, $groupstudents, $whichattempts, $reporturl);
             echo $this->everything_download_options();
             $quizinfo = $quizstats->get_formatted_quiz_info_data($course, $cm, $quiz);
             echo $this->output_quiz_info_table($quizinfo);
             if ($quizstats->s()) {
                 echo $OUTPUT->heading(get_string('quizstructureanalysis', 'quiz_statistics'));
                 $this->output_quiz_structure_analysis_table($quizstats->s(), $questionstats, $subquestionstats);
-                $this->output_statistics_graph($quiz->id, $currentgroup, $useallattempts);
+                $this->output_statistics_graph($quiz->id, $currentgroup, $whichattempts);
             }
         }
 
@@ -471,14 +468,14 @@ class quiz_statistics_report extends quiz_default_report {
      * Output the HTML needed to show the statistics graph.
      * @param $quizid
      * @param $currentgroup
-     * @param $useallattempts
+     * @param $whichattempts
      */
-    protected function output_statistics_graph($quizid, $currentgroup, $useallattempts) {
+    protected function output_statistics_graph($quizid, $currentgroup, $whichattempts) {
         global $PAGE;
 
         $output = $PAGE->get_renderer('mod_quiz');
         $imageurl = new moodle_url('/mod/quiz/report/statistics/statistics_graph.php',
-                                    compact('quizid', 'currentgroup', 'useallattempts'));
+                                    compact('quizid', 'currentgroup', 'whichattempts'));
         $graphname = get_string('statisticsreportgraph', 'quiz_statistics');
         echo $output->graph($imageurl, $graphname);
     }
@@ -488,18 +485,20 @@ class quiz_statistics_report extends quiz_default_report {
      * or by recomputing them.
      *
      * @param object $quiz the quiz settings.
-     * @param int $currentgroup the current group. 0 for none.
-     * @param bool $useallattempts use all attempts, or just first attempts.
+     * @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 question definitions.
+     * @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.
      */
-    protected function get_quiz_and_questions_stats($quiz, $currentgroup, $useallattempts, $groupstudents, $questions) {
+    protected function get_quiz_and_questions_stats($quiz, $whichattempts, $groupstudents, $questions) {
 
-        $qubaids = quiz_statistics_qubaids_condition($quiz->id, $currentgroup, $groupstudents, $useallattempts);
+        $qubaids = quiz_statistics_qubaids_condition($quiz->id, $groupstudents, $whichattempts);
 
         $qcalc = new \core_question\statistics\questions\calculator($questions);
 
@@ -509,8 +508,8 @@ class quiz_statistics_report extends quiz_default_report {
             // Recalculate now.
             list($questionstats, $subquestionstats) = $qcalc->calculate($qubaids);
 
-            $quizstats = $quizcalc->calculate($quiz->id, $currentgroup, $useallattempts,
-                                               $groupstudents, count($questions), $qcalc->get_sum_of_mark_variance());
+            $quizstats = $quizcalc->calculate($quiz->id, $whichattempts, $groupstudents, count($questions),
+                                              $qcalc->get_sum_of_mark_variance());
 
             if ($quizstats->s()) {
                 $this->analyse_responses_for_all_questions_and_subquestions($qubaids, $questions, $subquestionstats);
@@ -574,16 +573,16 @@ class quiz_statistics_report extends quiz_default_report {
      * with a recalcuate now button.
      * @param object $quizstats      the overall quiz statistics.
      * @param int    $quizid         the quiz id.
-     * @param int    $currentgroup   the id of the currently selected group, or 0.
-     * @param array  $groupstudents  ids of students in the group.
-     * @param bool   $useallattempts whether to use all attempts, instead of just
-     *                               first attempts.
+     * @param array  $groupstudents  ids of students in the group or empty array if groups not used.
+     * @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 moodle_url $reporturl url for this report
      * @return string a HTML snipped saying when the stats were last computed,
      *      or blank if that is not appropriate.
      */
-    protected function output_caching_info($quizstats, $quizid, $currentgroup,
-            $groupstudents, $useallattempts, $reporturl) {
+    protected function output_caching_info($quizstats, $quizid, $groupstudents, $whichattempts, $reporturl) {
         global $DB, $OUTPUT;
 
         if (empty($quizstats->timemodified)) {
@@ -591,8 +590,7 @@ class quiz_statistics_report extends quiz_default_report {
         }
 
         // Find the number of attempts since the cached statistics were computed.
-        list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql(
-                $quizid, $currentgroup, $groupstudents, $useallattempts, true);
+        list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql($quizid, $groupstudents, $whichattempts, true);
         $count = $DB->count_records_sql("
                 SELECT COUNT(1)
                 FROM $fromqa
index 02fb2f4..18071eb 100644 (file)
@@ -36,17 +36,15 @@ class quiz_statistics_settings_form extends moodleform {
     protected function definition() {
         $mform = $this->_form;
 
-        $mform->addElement('header', 'preferencespage',
-                get_string('preferencespage', 'quiz_overview'));
+        $mform->addElement('header', 'preferencespage', get_string('reportsettings', 'quiz_statistics'));
 
         $options = array();
-        $options[0] = get_string('attemptsfirst', 'quiz_statistics');
-        $options[1] = get_string('attemptsall', 'quiz_statistics');
-        $mform->addElement('select', 'useallattempts',
-                get_string('calculatefrom', 'quiz_statistics'), $options);
-        $mform->setDefault('useallattempts', 0);
-
-        $mform->addElement('submit', 'submitbutton',
-                get_string('preferencessave', 'quiz_overview'));
+        foreach (array_keys(quiz_get_grading_options()) as $which) {
+            $options[$which] = \quiz_statistics_calculator::using_attempts_lang_string($which);
+        }
+
+        $mform->addElement('select', 'whichattempts', get_string('calculatefrom', 'quiz_statistics'), $options);
+
+        $mform->addElement('submit', 'submitbutton', get_string('preferencessave', 'quiz_overview'));
     }
 }
index bb21b0f..de6a3f0 100644 (file)
@@ -37,7 +37,7 @@ require_once($CFG->dirroot . '/mod/quiz/report/statistics/statisticslib.php');
 // Get the parameters.
 $quizid = required_param('quizid', PARAM_INT);
 $currentgroup = required_param('currentgroup', PARAM_INT);
-$useallattempts = required_param('useallattempts', PARAM_INT);
+$whichattempts = required_param('whichattempts', PARAM_INT);
 
 $quiz = $DB->get_record('quiz', array('id' => $quizid), '*', MUST_EXIST);
 $cm = get_coursemodule_from_instance('quiz', $quiz->id);
@@ -55,10 +55,14 @@ if (groups_get_activity_groupmode($cm)) {
 if ($currentgroup && !in_array($currentgroup, array_keys($groups))) {
     print_error('groupnotamember', 'group');
 }
-$groupstudents = get_users_by_capability($modcontext, array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'),
-                                         '', '', '', '', $currentgroup, '', false);
 
-$qubaids = quiz_statistics_qubaids_condition($quizid, $currentgroup, $groupstudents, $useallattempts);
+if (empty($currentgroup)) {
+    $groupstudents = array();
+} else {
+    $groupstudents = get_users_by_capability($modcontext, array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'),
+                                             '', '', '', '', $currentgroup, '', false);
+}
+$qubaids = quiz_statistics_qubaids_condition($quizid, $groupstudents, $whichattempts);
 
 // Load the rest of the required data.
 $questions = quiz_report_get_significant_questions($quiz);
index c947dc8..b66b83d 100644 (file)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-function quiz_statistics_attempts_sql($quizid, $currentgroup, $groupstudents,
-                                      $allattempts = true, $includeungraded = false) {
+/**
+ * SQL to fetch relevant 'quiz_attempts' records.
+ *
+ * @param int    $quizid        quiz id to get attempts for
+ * @param array  $groupstudents empty array if not using groups or array of students in current group.
+ * @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 bool   $includeungraded whether to fetch ungraded attempts too
+ * @return array FROM and WHERE sql fragments and sql params
+ */
+function quiz_statistics_attempts_sql($quizid, $groupstudents, $whichattempts = QUIZ_GRADEAVERAGE, $includeungraded = false) {
     global $DB;
 
     $fromqa = '{quiz_attempts} quiza ';
@@ -32,15 +43,17 @@ function quiz_statistics_attempts_sql($quizid, $currentgroup, $groupstudents,
     $whereqa = 'quiza.quiz = :quizid AND quiza.preview = 0 AND quiza.state = :quizstatefinished';
     $qaparams = array('quizid' => (int)$quizid, 'quizstatefinished' => quiz_attempt::FINISHED);
 
-    if (!empty($currentgroup) && $groupstudents) {
+    if ($groupstudents) {
+        ksort($groupstudents);
         list($grpsql, $grpparams) = $DB->get_in_or_equal(array_keys($groupstudents),
                                                          SQL_PARAMS_NAMED, 'u');
         $whereqa .= " AND quiza.userid $grpsql";
         $qaparams += $grpparams;
     }
 
-    if (!$allattempts) {
-        $whereqa .= ' AND quiza.attempt = 1';
+    $whichattemptsql = quiz_report_grade_method_sql($whichattempts);
+    if ($whichattemptsql) {
+        $whereqa .= ' AND '.$whichattemptsql;
     }
 
     if (!$includeungraded) {
@@ -54,16 +67,16 @@ function quiz_statistics_attempts_sql($quizid, $currentgroup, $groupstudents,
  * Return a {@link qubaid_condition} from the values returned by {@link quiz_statistics_attempts_sql}.
  *
  * @param int     $quizid
- * @param int     $currentgroup
  * @param array   $groupstudents
- * @param bool    $allattempts
+ * @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 bool    $includeungraded
  * @return        \qubaid_join
  */
-function quiz_statistics_qubaids_condition($quizid, $currentgroup, $groupstudents,
-                                           $allattempts = true, $includeungraded = false) {
-    list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql($quizid, $currentgroup,
-                                                                      $groupstudents, $allattempts, $includeungraded);
+function quiz_statistics_qubaids_condition($quizid, $groupstudents, $whichattempts = QUIZ_GRADEAVERAGE, $includeungraded = false) {
+    list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql($quizid, $groupstudents, $whichattempts, $includeungraded);
     return new qubaid_join($fromqa, 'quiza.uniqueid', $whereqa, $qaparams);
 }
 
index 94c3a8b..831ed28 100644 (file)
@@ -40,12 +40,11 @@ require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php');
  */
 class testable_quiz_statistics_report extends quiz_statistics_report {
 
-    public function get_stats($quiz, $useallattempts = true,
-                              $currentgroup = 0, $groupstudents = array(), $nostudentsingroup = false) {
-        $qubaids = quiz_statistics_qubaids_condition($quiz->id, $currentgroup, $groupstudents, $useallattempts);
+    public function get_stats($quiz, $whichattempts = QUIZ_GRADEAVERAGE, $groupstudents = array()) {
+        $qubaids = quiz_statistics_qubaids_condition($quiz->id, $groupstudents, $whichattempts);
         $this->clear_cached_data($qubaids);
         $questions = $this->load_and_initialise_questions_for_calculations($quiz);
-        return $this->get_quiz_and_questions_stats($quiz, $currentgroup, $useallattempts, $groupstudents, $questions);
+        return $this->get_quiz_and_questions_stats($quiz, $whichattempts, $groupstudents, $questions);
     }
 }
 
index 8151693..f2f9a9c 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2013092000;
-$plugin->requires  = 2013092000;
+$plugin->version   = 2013093000;
+$plugin->requires  = 2013092700;
 $plugin->cron      = 18000;
 $plugin->component = 'quiz_statistics';