Merge branch 'int_behat' of https://github.com/skodak/moodle
authorSam Hemelryk <sam@moodle.com>
Tue, 24 Sep 2013 09:16:29 +0000 (21:16 +1200)
committerSam Hemelryk <sam@moodle.com>
Tue, 24 Sep 2013 09:16:29 +0000 (21:16 +1200)
24 files changed:
auth/cas/auth.php
backup/moodle2/backup_stepslib.php
backup/util/dbops/backup_structure_dbops.class.php
backup/util/plan/backup_structure_step.class.php
backup/util/structure/backup_structure_processor.class.php
lib/db/install.xml
lib/db/upgrade.php
mod/quiz/report/statistics/db/install.xml
mod/quiz/report/statistics/db/upgrade.php
mod/quiz/report/statistics/lib.php
mod/quiz/report/statistics/qstats.php [deleted file]
mod/quiz/report/statistics/report.php
mod/quiz/report/statistics/statistics_graph.php
mod/quiz/report/statistics/statistics_question_table.php
mod/quiz/report/statistics/statisticslib.php [new file with mode: 0644]
mod/quiz/report/statistics/tests/statistics_test.php
mod/quiz/report/statistics/tests/stats_from_steps_walkthrough_test.php
mod/quiz/report/statistics/version.php
question/engine/bank.php
question/engine/datalib.php
question/engine/responseanalysis.php [moved from mod/quiz/report/statistics/responseanalysis.php with 82% similarity]
question/engine/statistics.php [new file with mode: 0644]
question/engine/statisticslib.php [new file with mode: 0644]
version.php

index 634e32b..e748bbf 100644 (file)
@@ -96,6 +96,7 @@ class auth_plugin_cas extends auth_plugin_ldap {
         $site = get_site();
         $CASform = get_string('CASform', 'auth_cas');
         $username = optional_param('username', '', PARAM_RAW);
+        $courseid = optional_param('courseid', 0, PARAM_INT);
 
         if (!empty($username)) {
             if (isset($SESSION->wantsurl) && (strstr($SESSION->wantsurl, 'ticket') ||
@@ -117,6 +118,12 @@ class auth_plugin_cas extends auth_plugin_ldap {
             $frm = new stdClass();
             $frm->username = phpCAS::getUser();
             $frm->password = 'passwdCas';
+
+            // Redirect to a course if multi-auth is activated, authCAS is set to CAS and the courseid is specified.
+            if ($this->config->multiauth && !empty($courseid)) {
+                redirect(new moodle_url('/course/view.php', array('id'=>$courseid)));
+            }
+
             return;
         }
 
index e2f47f6..9437714 100644 (file)
@@ -1499,10 +1499,16 @@ class move_inforef_annotations_to_final extends backup_execution_step {
 
         // Items we want to include in the inforef file
         $items = backup_helper::get_inforef_itemnames();
+        $progress = $this->task->get_progress();
+        $progress->start_progress($this->get_name(), count($items));
+        $done = 1;
         foreach ($items as $itemname) {
             // Delegate to dbops
-            backup_structure_dbops::move_annotations_to_final($this->get_backupid(), $itemname);
+            backup_structure_dbops::move_annotations_to_final($this->get_backupid(),
+                    $itemname, $progress);
+            $progress->progress($done++);
         }
+        $progress->end_progress();
     }
 }
 
@@ -1667,7 +1673,11 @@ class backup_main_structure_step extends backup_structure_step {
 /**
  * Execution step that will generate the final zip (.mbz) file with all the contents
  */
-class backup_zip_contents extends backup_execution_step {
+class backup_zip_contents extends backup_execution_step implements file_progress {
+    /**
+     * @var bool True if we have started tracking progress
+     */
+    protected $startedprogress;
 
     protected function define_execution() {
 
@@ -1694,8 +1704,34 @@ class backup_zip_contents extends backup_execution_step {
         $zippacker = get_file_packer('application/zip');
 
         // Zip files
-        $zippacker->archive_to_pathname($files, $zipfile);
+        $zippacker->archive_to_pathname($files, $zipfile, true, $this);
+
+        // If any progress happened, end it.
+        if ($this->startedprogress) {
+            $this->task->get_progress()->end_progress();
+        }
     }
+
+    /**
+     * Implementation for file_progress interface to display unzip progress.
+     *
+     * @param int $progress Current progress
+     * @param int $max Max value
+     */
+    public function progress($progress = file_progress::INDETERMINATE, $max = file_progress::INDETERMINATE) {
+        $reporter = $this->task->get_progress();
+
+        // Start tracking progress if necessary.
+        if (!$this->startedprogress) {
+            $reporter->start_progress('extract_file_to_dir', ($max == file_progress::INDETERMINATE)
+                    ? core_backup_progress::INDETERMINATE : $max);
+            $this->startedprogress = true;
+        }
+
+        // Pass progress through to whatever handles it.
+        $reporter->progress(($progress == file_progress::INDETERMINATE)
+                ? core_backup_progress::INDETERMINATE : $progress);
+     }
 }
 
 /**
@@ -1955,6 +1991,8 @@ class backup_annotate_all_user_files extends backup_execution_step {
         // Fetch all annotated (final) users
         $rs = $DB->get_recordset('backup_ids_temp', array(
             'backupid' => $this->get_backupid(), 'itemname' => 'userfinal'));
+        $progress = $this->task->get_progress();
+        $progress->start_progress($this->get_name());
         foreach ($rs as $record) {
             $userid = $record->itemid;
             $userctx = context_user::instance($userid, IGNORE_MISSING);
@@ -1966,8 +2004,10 @@ class backup_annotate_all_user_files extends backup_execution_step {
                 // We don't need to specify itemid ($userid - 5th param) as far as by
                 // context we can get all the associated files. See MDL-22092
                 backup_structure_dbops::annotate_files($this->get_backupid(), $userctx->id, 'user', $filearea, null);
+                $progress->progress();
             }
         }
+        $progress->end_progress();
         $rs->close();
     }
 }
index 29529f0..61a903a 100644 (file)
@@ -130,10 +130,16 @@ abstract class backup_structure_dbops extends backup_dbops {
     /**
      * Moves all the existing 'item' annotations to their final 'itemfinal' ones
      * for a given backup.
+     *
+     * @param string $backupid Backup ID
+     * @param string $itemname Item name
+     * @param core_backup_progress $progress Progress tracker
      */
-    public static function move_annotations_to_final($backupid, $itemname) {
+    public static function move_annotations_to_final($backupid, $itemname, core_backup_progress $progress) {
         global $DB;
+        $progress->start_progress('move_annotations_to_final');
         $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $backupid, 'itemname' => $itemname));
+        $progress->progress();
         foreach($rs as $annotation) {
             // If corresponding 'itemfinal' annotation does not exist, update 'item' to 'itemfinal'
             if (! $DB->record_exists('backup_ids_temp', array('backupid' => $backupid,
@@ -141,10 +147,12 @@ abstract class backup_structure_dbops extends backup_dbops {
                                                               'itemid' => $annotation->itemid))) {
                 $DB->set_field('backup_ids_temp', 'itemname', $itemname . 'final', array('id' => $annotation->id));
             }
+            $progress->progress();
         }
         $rs->close();
         // All the remaining $itemname annotations can be safely deleted
         $DB->delete_records('backup_ids_temp', array('backupid' => $backupid, 'itemname' => $itemname));
+        $progress->end_progress();
     }
 
     /**
index 9c3d0e7..5ff5d08 100644 (file)
@@ -73,7 +73,9 @@ abstract class backup_structure_step extends backup_step {
                                              // from xml_writer (blame serialized data!)
         }
         $xw = new xml_writer($xo, $xt);
-        $pr = new backup_structure_processor($xw);
+        $progress = $this->task->get_progress();
+        $progress->start_progress($this->get_name());
+        $pr = new backup_structure_processor($xw, $progress);
 
         // Set processor variables from settings
         foreach ($this->get_settings() as $setting) {
@@ -105,6 +107,7 @@ abstract class backup_structure_step extends backup_step {
 
         // Close everything
         $xw->stop();
+        $progress->end_progress();
 
         // Destroy the structure. It helps PHP 5.2 memory a lot!
         $structure->destroy();
index 3c64854..88cfeca 100644 (file)
@@ -37,9 +37,21 @@ class backup_structure_processor extends base_processor {
     protected $writer; // xml_writer where the processor is going to output data
     protected $vars;   // array of backup::VAR_XXX => helper value pairs to be used by source specifications
 
-    public function __construct(xml_writer $writer) {
+    /**
+     * @var core_backup_progress Progress tracker (null if none)
+     */
+    protected $progress;
+
+    /**
+     * Constructor.
+     *
+     * @param xml_writer $writer XML writer to save data
+     * @param core_backup_progress $progress Progress tracker (optional)
+     */
+    public function __construct(xml_writer $writer, core_backup_progress $progress = null) {
         $this->writer = $writer;
-        $this->vars   = array();
+        $this->progress = $progress;
+        $this->vars = array();
     }
 
     public function set_var($key, $value) {
@@ -83,6 +95,9 @@ class backup_structure_processor extends base_processor {
     public function post_process_nested_element(base_nested_element $nested) {
         // Send close tag to xml_writer
         $this->writer->end_tag($nested->get_name());
+        if ($this->progress) {
+            $this->progress->progress();
+        }
     }
 
     public function process_final_element(base_final_element $final) {
@@ -93,6 +108,9 @@ class backup_structure_processor extends base_processor {
                 $attrarr[$attribute->get_name()] = $attribute->get_value();
             }
             $this->writer->full_tag($final->get_name(), $final->get_value(), $attrarr);
+            if ($this->progress) {
+                $this->progress->progress();
+            }
             // Annotate current value if configured to do so
             $final->annotate($this->get_var(backup::VAR_BACKUPID));
         }
index d79981f..18cb636 100644 (file)
         <INDEX NAME="attemptid-questionid" UNIQUE="true" FIELDS="attemptid, questionid"/>
       </INDEXES>
     </TABLE>
+      <TABLE NAME="question_statistics" COMMENT="Statistics for individual questions used in an activity.">
+          <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="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+              <FIELD NAME="questionid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+              <FIELD NAME="slot" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="The position in the quiz where this question appears"/>
+              <FIELD NAME="subquestion" TYPE="int" LENGTH="4" NOTNULL="true" SEQUENCE="false"/>
+              <FIELD NAME="s" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
+              <FIELD NAME="effectiveweight" TYPE="number" LENGTH="15" NOTNULL="false" SEQUENCE="false" DECIMALS="5"/>
+              <FIELD NAME="negcovar" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
+              <FIELD NAME="discriminationindex" TYPE="number" LENGTH="15" NOTNULL="false" SEQUENCE="false" DECIMALS="5"/>
+              <FIELD NAME="discriminativeefficiency" TYPE="number" LENGTH="15" NOTNULL="false" SEQUENCE="false" DECIMALS="5"/>
+              <FIELD NAME="sd" TYPE="number" LENGTH="15" NOTNULL="false" SEQUENCE="false" DECIMALS="10"/>
+              <FIELD NAME="facility" TYPE="number" LENGTH="15" NOTNULL="false" SEQUENCE="false" DECIMALS="10"/>
+              <FIELD NAME="subquestions" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
+              <FIELD NAME="maxmark" TYPE="number" LENGTH="12" NOTNULL="false" SEQUENCE="false" DECIMALS="7"/>
+              <FIELD NAME="positions" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="positions in which this item appears. Only used for random questions."/>
+              <FIELD NAME="randomguessscore" TYPE="number" LENGTH="12" NOTNULL="false" SEQUENCE="false" DECIMALS="7" COMMENT="An estimate of the score a student would get by guessing randomly."/>
+          </FIELDS>
+          <KEYS>
+              <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+          </KEYS>
+      </TABLE>
+      <TABLE NAME="question_response_analysis" COMMENT="Analysis of student responses given to questions.">
+          <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="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+              <FIELD NAME="questionid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+              <FIELD NAME="subqid" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false"/>
+              <FIELD NAME="aid" TYPE="char" LENGTH="100" NOTNULL="false" SEQUENCE="false"/>
+              <FIELD NAME="response" TYPE="text" LENGTH="big" NOTNULL="false" SEQUENCE="false"/>
+              <FIELD NAME="rcount" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/>
+              <FIELD NAME="credit" TYPE="number" LENGTH="15" NOTNULL="true" SEQUENCE="false" DECIMALS="5"/>
+          </FIELDS>
+          <KEYS>
+              <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+          </KEYS>
+      </TABLE>
     <TABLE NAME="mnet_application" COMMENT="Information about applications on remote hosts">
       <FIELDS>
         <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
       </KEYS>
     </TABLE>
   </TABLES>
-</XMLDB>
\ No newline at end of file
+</XMLDB>
index 543d1b3..7a9547a 100644 (file)
@@ -2450,5 +2450,63 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2013091300.01);
     }
 
+    if ($oldversion < 2013092000.01) {
+
+        // Define table question_statistics to be created.
+        $table = new xmldb_table('question_statistics');
+
+        // Adding fields to table question_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('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('questionid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('slot', XMLDB_TYPE_INTEGER, '10', null, null, null, null);
+        $table->add_field('subquestion', XMLDB_TYPE_INTEGER, '4', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('s', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
+        $table->add_field('effectiveweight', XMLDB_TYPE_NUMBER, '15, 5', null, null, null, null);
+        $table->add_field('negcovar', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0');
+        $table->add_field('discriminationindex', XMLDB_TYPE_NUMBER, '15, 5', null, null, null, null);
+        $table->add_field('discriminativeefficiency', XMLDB_TYPE_NUMBER, '15, 5', null, null, null, null);
+        $table->add_field('sd', XMLDB_TYPE_NUMBER, '15, 10', null, null, null, null);
+        $table->add_field('facility', XMLDB_TYPE_NUMBER, '15, 10', null, null, null, null);
+        $table->add_field('subquestions', XMLDB_TYPE_TEXT, null, null, null, null, null);
+        $table->add_field('maxmark', XMLDB_TYPE_NUMBER, '12, 7', null, null, null, null);
+        $table->add_field('positions', XMLDB_TYPE_TEXT, null, null, null, null, null);
+        $table->add_field('randomguessscore', XMLDB_TYPE_NUMBER, '12, 7', null, null, null, null);
+
+        // Adding keys to table question_statistics.
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+
+        // Conditionally launch create table for question_statistics.
+        if (!$dbman->table_exists($table)) {
+            $dbman->create_table($table);
+        }
+
+        // Define table question_response_analysis to be created.
+        $table = new xmldb_table('question_response_analysis');
+
+        // Adding fields to table question_response_analysis.
+        $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('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('questionid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('subqid', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('aid', XMLDB_TYPE_CHAR, '100', null, null, null, null);
+        $table->add_field('response', XMLDB_TYPE_TEXT, null, null, null, null, null);
+        $table->add_field('rcount', XMLDB_TYPE_INTEGER, '10', null, null, null, null);
+        $table->add_field('credit', XMLDB_TYPE_NUMBER, '15, 5', null, XMLDB_NOTNULL, null, null);
+
+        // Adding keys to table question_response_analysis.
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+
+        // Conditionally launch create table for question_response_analysis.
+        if (!$dbman->table_exists($table)) {
+            $dbman->create_table($table);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2013092000.01);
+    }
+
     return true;
 }
index 1a56de2..853270d 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="mod/quiz/report/statistics/db" VERSION="20120122" COMMENT="XMLDB file for Moodle mod/quiz/report/statistics"
+<XMLDB PATH="mod/quiz/report/statistics/db" VERSION="20130920" COMMENT="XMLDB file for Moodle mod/quiz/report/statistics"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../../../../lib/xmldb/xmldb.xsd"
 >
@@ -7,8 +7,7 @@
     <TABLE NAME="quiz_statistics" COMMENT="table to cache results from analysis done in statistics report for quizzes.">
       <FIELDS>
         <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
-        <FIELD NAME="quizid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
-        <FIELD NAME="groupid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <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="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="firstattemptscount" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
       </KEYS>
     </TABLE>
-    <TABLE NAME="quiz_question_statistics" COMMENT="Default comment for the table, please edit me">
-      <FIELDS>
-        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
-        <FIELD NAME="quizstatisticsid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
-        <FIELD NAME="questionid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
-        <FIELD NAME="slot" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="The position in the quiz where this question appears"/>
-        <FIELD NAME="subquestion" TYPE="int" LENGTH="4" NOTNULL="true" SEQUENCE="false"/>
-        <FIELD NAME="s" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
-        <FIELD NAME="effectiveweight" TYPE="number" LENGTH="15" NOTNULL="false" SEQUENCE="false" DECIMALS="5"/>
-        <FIELD NAME="negcovar" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
-        <FIELD NAME="discriminationindex" TYPE="number" LENGTH="15" NOTNULL="false" SEQUENCE="false" DECIMALS="5"/>
-        <FIELD NAME="discriminativeefficiency" TYPE="number" LENGTH="15" NOTNULL="false" SEQUENCE="false" DECIMALS="5"/>
-        <FIELD NAME="sd" TYPE="number" LENGTH="15" NOTNULL="false" SEQUENCE="false" DECIMALS="10"/>
-        <FIELD NAME="facility" TYPE="number" LENGTH="15" NOTNULL="false" SEQUENCE="false" DECIMALS="10"/>
-        <FIELD NAME="subquestions" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
-        <FIELD NAME="maxmark" TYPE="number" LENGTH="12" NOTNULL="false" SEQUENCE="false" DECIMALS="7"/>
-        <FIELD NAME="positions" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="positions in which this item appears. Only used for random questions."/>
-        <FIELD NAME="randomguessscore" TYPE="number" LENGTH="12" NOTNULL="false" SEQUENCE="false" DECIMALS="7" COMMENT="An estimate of the score a student would get by guessing randomly."/>
-      </FIELDS>
-      <KEYS>
-        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
-      </KEYS>
-    </TABLE>
-    <TABLE NAME="quiz_question_response_stats" COMMENT="Quiz question responses.">
-      <FIELDS>
-        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
-        <FIELD NAME="quizstatisticsid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
-        <FIELD NAME="questionid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
-        <FIELD NAME="subqid" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false"/>
-        <FIELD NAME="aid" TYPE="char" LENGTH="100" NOTNULL="false" SEQUENCE="false"/>
-        <FIELD NAME="response" TYPE="text" LENGTH="big" NOTNULL="false" SEQUENCE="false"/>
-        <FIELD NAME="rcount" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/>
-        <FIELD NAME="credit" TYPE="number" LENGTH="15" NOTNULL="true" SEQUENCE="false" DECIMALS="5"/>
-      </FIELDS>
-      <KEYS>
-        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
-      </KEYS>
-    </TABLE>
   </TABLES>
 </XMLDB>
index 1e87bf3..4beb458 100644 (file)
@@ -37,43 +37,55 @@ function xmldb_quiz_statistics_upgrade($oldversion) {
     // Moodle v2.2.0 release upgrade line.
     // Put any upgrade step following this.
 
-    if ($oldversion < 2012061800) {
+    // Moodle v2.3.0 release upgrade line
+    // Put any upgrade step following this
 
-        // Changing type of field subqid on table quiz_question_response_stats to char.
-        $table = new xmldb_table('quiz_question_response_stats');
-        $field = new xmldb_field('subqid', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null, 'questionid');
+    // Moodle v2.4.0 release upgrade line
+    // Put any upgrade step following this
 
-        // Launch change of type for field subqid.
-        $dbman->change_field_type($table, $field);
+    // Moodle v2.5.0 release upgrade line.
+    // Put any upgrade step following this.
 
-        // Statistics savepoint reached.
-        upgrade_plugin_savepoint(true, 2012061800, 'quiz', 'statistics');
-    }
+    if ($oldversion < 2013092000) {
+
+        // Define table question_statistics to be dropped.
+        $table = new xmldb_table('quiz_question_statistics');
 
-    if ($oldversion < 2012061801) {
+        // Conditionally launch drop table for question_statistics.
+        if ($dbman->table_exists($table)) {
+            $dbman->drop_table($table);
+        }
 
-        // Changing type of field aid on table quiz_question_response_stats to char.
+        // Define table question_response_analysis to be dropped.
         $table = new xmldb_table('quiz_question_response_stats');
-        $field = new xmldb_field('aid', XMLDB_TYPE_CHAR, '100', null, null, null, null, 'subqid');
 
-        // Launch change of type for field aid.
-        $dbman->change_field_type($table, $field);
+        // Conditionally launch drop table for question_response_analysis.
+        if ($dbman->table_exists($table)) {
+            $dbman->drop_table($table);
+        }
 
-        // Statistics savepoint reached.
-        upgrade_plugin_savepoint(true, 2012061801, 'quiz', 'statistics');
-    }
+        $table = new xmldb_table('quiz_statistics');
+        $field = new xmldb_field('quizid');
 
-    // Moodle v2.3.0 release upgrade line
-    // Put any upgrade step following this
+        if ($dbman->field_exists($table, $field)) {
+            $dbman->drop_field($table, $field);
+        }
 
+        $field = new xmldb_field('groupid');
 
-    // Moodle v2.4.0 release upgrade line
-    // Put any upgrade step following this
+        if ($dbman->field_exists($table, $field)) {
+            $dbman->drop_field($table, $field);
+        }
 
+        $field = new xmldb_field('hashcode', XMLDB_TYPE_CHAR, '40', null, XMLDB_NOTNULL, null, null, 'id');
 
-    // Moodle v2.5.0 release upgrade line.
-    // Put any upgrade step following this.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
 
+        // Main savepoint reached.
+        upgrade_plugin_savepoint(true, 2013092000, 'quiz', 'statistics');
+    }
 
     return true;
 }
index 081d2a8..2dc0751 100644 (file)
@@ -68,24 +68,10 @@ function quiz_statistics_question_preview_pluginfile($previewcontext, $questioni
 function quiz_statistics_cron() {
     global $DB;
 
-    $expiretime = time() - 5*HOURSECS;
-    $todelete = $DB->get_records_select_menu('quiz_statistics',
-            'timemodified < ?', array($expiretime), '', 'id, 1');
-
-    if (!$todelete) {
-        return true;
-    }
-
-    list($todeletesql, $todeleteparams) = $DB->get_in_or_equal(array_keys($todelete));
+    mtrace("\n  Cleaning up old quiz statistics cache records...", '');
 
-    $DB->delete_records_select('quiz_question_statistics',
-            'quizstatisticsid ' . $todeletesql, $todeleteparams);
-
-    $DB->delete_records_select('quiz_question_response_stats',
-            'quizstatisticsid ' . $todeletesql, $todeleteparams);
-
-    $DB->delete_records_select('quiz_statistics',
-            'id ' . $todeletesql, $todeleteparams);
+    $expiretime = time() - 5*HOURSECS;
+    $DB->delete_records_select('quiz_statistics', 'timemodified < ?', array($expiretime));
 
     return true;
 }
diff --git a/mod/quiz/report/statistics/qstats.php b/mod/quiz/report/statistics/qstats.php
deleted file mode 100644 (file)
index 75001bf..0000000
+++ /dev/null
@@ -1,405 +0,0 @@
-<?php
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * Quiz statistics report calculations class.
- *
- * @package   quiz_statistics
- * @copyright 2008 Jamie Pratt
- * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-
-defined('MOODLE_INTERNAL') || die();
-
-
-/**
- * This class has methods to compute the question statistics from the raw data.
- *
- * @copyright 2008 Jamie Pratt
- * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class quiz_statistics_question_stats {
-    public $questions;
-    public $subquestions = array();
-
-    protected $s;
-    protected $summarksavg;
-    protected $allattempts;
-
-    /** @var mixed states from which to calculate stats - iteratable. */
-    protected $lateststeps;
-
-    protected $sumofmarkvariance = 0;
-    protected $randomselectors = array();
-
-    /**
-     * Constructor.
-     * @param $questions the questions.
-     * @param $s the number of attempts included in the stats.
-     * @param $summarksavg the average attempt summarks.
-     */
-    public function __construct($questions, $s, $summarksavg) {
-        $this->s = $s;
-        $this->summarksavg = $summarksavg;
-
-        foreach ($questions as $slot => $question) {
-            $question->_stats = $this->make_blank_question_stats();
-            $question->_stats->questionid = $question->id;
-            $question->_stats->slot = $slot;
-        }
-
-        $this->questions = $questions;
-    }
-
-    /**
-     * @return object ready to hold all the question statistics.
-     */
-    protected function make_blank_question_stats() {
-        $stats = new stdClass();
-        $stats->slot = null;
-        $stats->s = 0;
-        $stats->totalmarks = 0;
-        $stats->totalothermarks = 0;
-        $stats->markvariancesum = 0;
-        $stats->othermarkvariancesum = 0;
-        $stats->covariancesum = 0;
-        $stats->covariancemaxsum = 0;
-        $stats->subquestion = false;
-        $stats->subquestions = '';
-        $stats->covariancewithoverallmarksum = 0;
-        $stats->randomguessscore = null;
-        $stats->markarray = array();
-        $stats->othermarksarray = array();
-        return $stats;
-    }
-
-    /**
-     * Load the data that will be needed to perform the calculations.
-     *
-     * @param int $quizid the quiz id.
-     * @param int $currentgroup the current group. 0 for none.
-     * @param array $groupstudents students in this group.
-     * @param bool $allattempts use all attempts, or just first attempts.
-     */
-    public function load_step_data($quizid, $currentgroup, $groupstudents, $allattempts) {
-        global $DB;
-
-        $this->allattempts = $allattempts;
-
-        list($qsql, $qparams) = $DB->get_in_or_equal(array_keys($this->questions),
-                SQL_PARAMS_NAMED, 'q');
-        list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql(
-                $quizid, $currentgroup, $groupstudents, $allattempts, false);
-
-        $this->lateststeps = $DB->get_records_sql("
-                SELECT
-                    qas.id,
-                    quiza.sumgrades,
-                    qa.questionid,
-                    qa.slot,
-                    qa.maxmark,
-                    qas.fraction * qa.maxmark as mark
-
-                FROM $fromqa
-                JOIN {question_attempts} qa ON qa.questionusageid = quiza.uniqueid
-                JOIN (
-                    SELECT questionattemptid, MAX(id) AS latestid
-                      FROM {question_attempt_steps}
-                  GROUP BY questionattemptid
-                ) lateststepid ON lateststepid.questionattemptid = qa.id
-                JOIN {question_attempt_steps} qas ON qas.id = lateststepid.latestid
-
-                WHERE
-                    qa.slot $qsql AND
-                    $whereqa", $qparams + $qaparams);
-    }
-
-    public function compute_statistics() {
-        set_time_limit(0);
-
-        $subquestionstats = array();
-
-        // Compute the statistics of position, and for random questions, work
-        // out which questions appear in which positions.
-        foreach ($this->lateststeps as $step) {
-            $this->initial_steps_walker($step, $this->questions[$step->slot]->_stats);
-
-            // If this is a random question what is the real item being used?
-            if ($step->questionid != $this->questions[$step->slot]->id) {
-                if (!isset($subquestionstats[$step->questionid])) {
-                    $subquestionstats[$step->questionid] = $this->make_blank_question_stats();
-                    $subquestionstats[$step->questionid]->questionid = $step->questionid;
-                    $subquestionstats[$step->questionid]->allattempts = $this->allattempts;
-                    $subquestionstats[$step->questionid]->usedin = array();
-                    $subquestionstats[$step->questionid]->subquestion = true;
-                    $subquestionstats[$step->questionid]->differentweights = false;
-                    $subquestionstats[$step->questionid]->maxmark = $step->maxmark;
-                } else if ($subquestionstats[$step->questionid]->maxmark != $step->maxmark) {
-                    $subquestionstats[$step->questionid]->differentweights = true;
-                }
-
-                $this->initial_steps_walker($step,
-                        $subquestionstats[$step->questionid], false);
-
-                $number = $this->questions[$step->slot]->number;
-                $subquestionstats[$step->questionid]->usedin[$number] = $number;
-
-                $randomselectorstring = $this->questions[$step->slot]->category .
-                        '/' . $this->questions[$step->slot]->questiontext;
-                if (!isset($this->randomselectors[$randomselectorstring])) {
-                    $this->randomselectors[$randomselectorstring] = array();
-                }
-                $this->randomselectors[$randomselectorstring][$step->questionid] =
-                        $step->questionid;
-            }
-        }
-
-        foreach ($this->randomselectors as $key => $notused) {
-            ksort($this->randomselectors[$key]);
-        }
-
-        // Compute the statistics of question id, if we need any.
-        $this->subquestions = question_load_questions(array_keys($subquestionstats));
-        foreach ($this->subquestions as $qid => $subquestion) {
-            $subquestion->_stats = $subquestionstats[$qid];
-            $subquestion->maxmark = $subquestion->_stats->maxmark;
-            $subquestion->_stats->randomguessscore = $this->get_random_guess_score($subquestion);
-
-            $this->initial_question_walker($subquestion->_stats);
-
-            if ($subquestionstats[$qid]->differentweights) {
-                // TODO output here really sucks, but throwing is too severe.
-                global $OUTPUT;
-                echo $OUTPUT->notification(
-                        get_string('erroritemappearsmorethanoncewithdifferentweight',
-                        'quiz_statistics', $this->subquestions[$qid]->name));
-            }
-
-            if ($subquestion->_stats->usedin) {
-                sort($subquestion->_stats->usedin, SORT_NUMERIC);
-                $subquestion->_stats->positions = implode(',', $subquestion->_stats->usedin);
-            } else {
-                $subquestion->_stats->positions = '';
-            }
-        }
-
-        // Finish computing the averages, and put the subquestion 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->questions);
-        while (list($slot, $question) = each($this->questions)) {
-            $nextquestion = current($this->questions);
-            $question->_stats->allattempts = $this->allattempts;
-            $question->_stats->positions = $question->number;
-            $question->_stats->maxmark = $question->maxmark;
-            $question->_stats->randomguessscore = $this->get_random_guess_score($question);
-
-            $this->initial_question_walker($question->_stats);
-
-            if ($question->qtype == 'random') {
-                $randomselectorstring = $question->category.'/'.$question->questiontext;
-                if ($nextquestion && $nextquestion->qtype == 'random') {
-                    $nextrandomselectorstring = $nextquestion->category . '/' .
-                            $nextquestion->questiontext;
-                    if ($randomselectorstring == $nextrandomselectorstring) {
-                        continue; // Next loop iteration.
-                    }
-                }
-                if (isset($this->randomselectors[$randomselectorstring])) {
-                    $question->_stats->subquestions = implode(',',
-                            $this->randomselectors[$randomselectorstring]);
-                }
-            }
-        }
-
-        // Go through the records one more time.
-        foreach ($this->lateststeps as $step) {
-            $this->secondary_steps_walker($step,
-                    $this->questions[$step->slot]->_stats);
-
-            if ($this->questions[$step->slot]->qtype == 'random') {
-                $this->secondary_steps_walker($step,
-                        $this->subquestions[$step->questionid]->_stats);
-            }
-        }
-
-        $sumofcovariancewithoverallmark = 0;
-        foreach ($this->questions as $slot => $question) {
-            $this->secondary_question_walker($question->_stats);
-
-            $this->sumofmarkvariance += $question->_stats->markvariance;
-
-            if ($question->_stats->covariancewithoverallmark >= 0) {
-                $sumofcovariancewithoverallmark +=
-                        sqrt($question->_stats->covariancewithoverallmark);
-                $question->_stats->negcovar = 0;
-            } else {
-                $question->_stats->negcovar = 1;
-            }
-        }
-
-        foreach ($this->subquestions as $subquestion) {
-            $this->secondary_question_walker($subquestion->_stats);
-        }
-
-        foreach ($this->questions as $question) {
-            if ($sumofcovariancewithoverallmark) {
-                if ($question->_stats->negcovar) {
-                    $question->_stats->effectiveweight = null;
-                } else {
-                    $question->_stats->effectiveweight = 100 *
-                            sqrt($question->_stats->covariancewithoverallmark) /
-                            $sumofcovariancewithoverallmark;
-                }
-            } else {
-                $question->_stats->effectiveweight = null;
-            }
-        }
-    }
-
-    /**
-     * Update $stats->totalmarks, $stats->markarray, $stats->totalothermarks
-     * and $stats->othermarksarray to include another state.
-     *
-     * @param object $step the state to add to the statistics.
-     * @param object $stats the question statistics we are accumulating.
-     * @param bool $positionstat whether this is a statistic of position of question.
-     */
-    protected function initial_steps_walker($step, $stats, $positionstat = true) {
-        $stats->s++;
-        $stats->totalmarks += $step->mark;
-        $stats->markarray[] = $step->mark;
-
-        if ($positionstat) {
-            $stats->totalothermarks += $step->sumgrades - $step->mark;
-            $stats->othermarksarray[] = $step->sumgrades - $step->mark;
-
-        } else {
-            $stats->totalothermarks += $step->sumgrades;
-            $stats->othermarksarray[] = $step->sumgrades;
-        }
-    }
-
-    /**
-     * Perform some computations on the per-question statistics calculations after
-     * we have been through all the states.
-     *
-     * @param object $stats quetsion stats to update.
-     */
-    protected function initial_question_walker($stats) {
-        $stats->markaverage = $stats->totalmarks / $stats->s;
-
-        if ($stats->maxmark != 0) {
-            $stats->facility = $stats->markaverage / $stats->maxmark;
-        } else {
-            $stats->facility = null;
-        }
-
-        $stats->othermarkaverage = $stats->totalothermarks / $stats->s;
-
-        sort($stats->markarray, SORT_NUMERIC);
-        sort($stats->othermarksarray, SORT_NUMERIC);
-    }
-
-    /**
-     * Now we know the averages, accumulate the date needed to compute the higher
-     * moments of the question scores.
-     *
-     * @param object $step the state to add to the statistics.
-     * @param object $stats the question statistics we are accumulating.
-     * @param bool $positionstat whether this is a statistic of position of question.
-     */
-    protected function secondary_steps_walker($step, $stats) {
-        $markdifference = $step->mark - $stats->markaverage;
-        if ($stats->subquestion) {
-            $othermarkdifference = $step->sumgrades - $stats->othermarkaverage;
-        } else {
-            $othermarkdifference = $step->sumgrades - $step->mark -
-                    $stats->othermarkaverage;
-        }
-        $overallmarkdifference = $step->sumgrades - $this->summarksavg;
-
-        $sortedmarkdifference = array_shift($stats->markarray) - $stats->markaverage;
-        $sortedothermarkdifference = array_shift($stats->othermarksarray) -
-                $stats->othermarkaverage;
-
-        $stats->markvariancesum += pow($markdifference, 2);
-        $stats->othermarkvariancesum += pow($othermarkdifference, 2);
-        $stats->covariancesum += $markdifference * $othermarkdifference;
-        $stats->covariancemaxsum += $sortedmarkdifference * $sortedothermarkdifference;
-        $stats->covariancewithoverallmarksum += $markdifference * $overallmarkdifference;
-    }
-
-    /**
-     * Perform more per-question statistics calculations.
-     *
-     * @param object $stats quetsion stats to update.
-     */
-    protected function secondary_question_walker($stats) {
-        if ($stats->s > 1) {
-            $stats->markvariance = $stats->markvariancesum / ($stats->s - 1);
-            $stats->othermarkvariance = $stats->othermarkvariancesum / ($stats->s - 1);
-            $stats->covariance = $stats->covariancesum / ($stats->s - 1);
-            $stats->covariancemax = $stats->covariancemaxsum / ($stats->s - 1);
-            $stats->covariancewithoverallmark = $stats->covariancewithoverallmarksum /
-                    ($stats->s - 1);
-            $stats->sd = sqrt($stats->markvariancesum / ($stats->s - 1));
-
-        } else {
-            $stats->markvariance = null;
-            $stats->othermarkvariance = null;
-            $stats->covariance = null;
-            $stats->covariancemax = null;
-            $stats->covariancewithoverallmark = null;
-            $stats->sd = null;
-        }
-
-        if ($stats->markvariance * $stats->othermarkvariance) {
-            $stats->discriminationindex = 100 * $stats->covariance /
-                    sqrt($stats->markvariance * $stats->othermarkvariance);
-        } else {
-            $stats->discriminationindex = null;
-        }
-
-        if ($stats->covariancemax) {
-            $stats->discriminativeefficiency = 100 * $stats->covariance /
-                    $stats->covariancemax;
-        } else {
-            $stats->discriminativeefficiency = null;
-        }
-    }
-
-    /**
-     * @param object $questiondata
-     * @return number the random guess score for this question.
-     */
-    protected function get_random_guess_score($questiondata) {
-        return question_bank::get_qtype(
-                $questiondata->qtype, false)->get_random_guess_score($questiondata);
-    }
-
-    /**
-     * Used when computing CIC.
-     * @return number
-     */
-    public function get_sum_of_mark_variance() {
-        return $this->sumofmarkvariance;
-    }
-}
index f30eca2..866794a 100644 (file)
@@ -28,9 +28,9 @@ defined('MOODLE_INTERNAL') || die();
 require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_form.php');
 require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_table.php');
 require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_question_table.php');
-require_once($CFG->dirroot . '/mod/quiz/report/statistics/qstats.php');
-require_once($CFG->dirroot . '/mod/quiz/report/statistics/responseanalysis.php');
-
+require_once($CFG->dirroot . '/question/engine/statistics.php');
+require_once($CFG->dirroot . '/question/engine/responseanalysis.php');
+require_once($CFG->dirroot . '/mod/quiz/report/statistics/statisticslib.php');
 
 /**
  * The quiz statistics report provides summary information about each question in
@@ -104,9 +104,12 @@ class quiz_statistics_report extends quiz_default_report {
             }
         }
 
+        $qubaids = quiz_statistics_qubaids_condition($quiz->id, $currentgroup, $groupstudents, $useallattempts);
+
+
         // If recalculate was requested, handle that.
         if ($recalculate && confirm_sesskey()) {
-            $this->clear_cached_data($quiz->id, $currentgroup, $useallattempts);
+            $this->clear_cached_data($qubaids);
             redirect($reporturl);
         }
 
@@ -164,21 +167,21 @@ class quiz_statistics_report extends quiz_default_report {
             if ($s) {
                 $this->output_quiz_structure_analysis_table($s, $questions, $subquestions);
 
-                if ($this->table->is_downloading() == 'xhtml') {
-                    $this->output_statistics_graph($quizstats->id, $s);
+                if ($this->table->is_downloading() == 'xhtml' && $s != 0) {
+                    $this->output_statistics_graph($quiz->id, $currentgroup, $useallattempts);
                 }
 
                 foreach ($questions as $question) {
                     if (question_bank::get_qtype(
                             $question->qtype, false)->can_analyse_responses()) {
                         $this->output_individual_question_response_analysis(
-                                $question, $reporturl, $quizstats);
+                                $question, $reporturl, $qubaids);
 
                     } else if (!empty($question->_stats->subquestions)) {
                         $subitemstodisplay = explode(',', $question->_stats->subquestions);
                         foreach ($subitemstodisplay as $subitemid) {
                             $this->output_individual_question_response_analysis(
-                                    $subquestions[$subitemid], $reporturl, $quizstats);
+                                    $subquestions[$subitemid], $reporturl, $qubaids);
                         }
                     }
                 }
@@ -194,7 +197,7 @@ class quiz_statistics_report extends quiz_default_report {
 
             $this->output_individual_question_data($quiz, $questions[$slot]);
             $this->output_individual_question_response_analysis(
-                    $questions[$slot], $reporturl, $quizstats);
+                    $questions[$slot], $reporturl, $qubaids);
 
             // Back to overview link.
             echo $OUTPUT->box('<a href="' . $reporturl->out() . '">' .
@@ -209,7 +212,7 @@ class quiz_statistics_report extends quiz_default_report {
 
             $this->output_individual_question_data($quiz, $subquestions[$qid]);
             $this->output_individual_question_response_analysis(
-                    $subquestions[$qid], $reporturl, $quizstats);
+                    $subquestions[$qid], $reporturl, $qubaids);
 
             // Back to overview link.
             echo $OUTPUT->box('<a href="' . $reporturl->out() . '">' .
@@ -232,7 +235,7 @@ class quiz_statistics_report extends quiz_default_report {
             if ($s) {
                 echo $OUTPUT->heading(get_string('quizstructureanalysis', 'quiz_statistics'));
                 $this->output_quiz_structure_analysis_table($s, $questions, $subquestions);
-                $this->output_statistics_graph($quizstats->id, $s);
+                $this->output_statistics_graph($quiz->id, $currentgroup, $useallattempts);
             }
         }
 
@@ -323,12 +326,12 @@ class quiz_statistics_report extends quiz_default_report {
 
     /**
      * Display the response analysis for a question.
-     * @param object $question the question to report on.
+     * @param object     $question  the question to report on.
      * @param moodle_url $reporturl the URL to resisplay this report.
-     * @param object $quizstats Holds the quiz statistics.
+     * @param qubaid_condition $qubaids
      */
     protected function output_individual_question_response_analysis($question,
-            $reporturl, $quizstats) {
+            $reporturl, $qubaids) {
         global $OUTPUT;
 
         if (!question_bank::get_qtype($question->qtype, false)->can_analyse_responses()) {
@@ -361,8 +364,8 @@ class quiz_statistics_report extends quiz_default_report {
             }
         }
 
-        $responesstats = new quiz_statistics_response_analyser($question);
-        $responesstats->load_cached($quizstats->id);
+        $responesstats = new question_response_analyser($question);
+        $responesstats->load_cached($qubaids);
 
         $qtable->question_setup($reporturl, $question, $responesstats);
         if ($this->table->is_downloading()) {
@@ -549,18 +552,16 @@ class quiz_statistics_report extends quiz_default_report {
 
     /**
      * Output the HTML needed to show the statistics graph.
-     * @param int $quizstatsid the id of the statistics to show in the graph.
+     * @param $quizid
+     * @param $currentgroup
+     * @param $useallattempts
      */
-    protected function output_statistics_graph($quizstatsid, $s) {
+    protected function output_statistics_graph($quizid, $currentgroup, $useallattempts) {
         global $PAGE;
 
-        if ($s == 0) {
-            return;
-        }
-
         $output = $PAGE->get_renderer('mod_quiz');
         $imageurl = new moodle_url('/mod/quiz/report/statistics/statistics_graph.php',
-                array('id' => $quizstatsid));
+                                    compact('quizid', 'currentgroup', 'useallattempts'));
         $graphname = get_string('statisticsreportgraph', 'quiz_statistics');
         echo $output->graph($imageurl, $graphname);
     }
@@ -568,53 +569,39 @@ class quiz_statistics_report extends quiz_default_report {
     /**
      * Return the stats data for when there are no stats to show.
      *
-     * @param array $questions question definitions.
      * @param int $firstattemptscount number of first attempts (optional).
-     * @param int $firstattemptscount total number of attempts (optional).
-     * @return array with three elements:
+     * @param int $allattemptscount total number of attempts (optional).
+     * @return array with two elements:
      *      - integer $s Number of attempts included in the stats (0).
-     *      - array $quizstats The statistics for overall attempt scores.
-     *      - array $qstats The statistics for each question.
+     *      - object $quizstats The statistics for overall attempt scores.
      */
-    protected function get_emtpy_stats($questions, $firstattemptscount = 0,
-            $allattemptscount = 0) {
+    protected function get_empty_stats($firstattemptscount = 0, $allattemptscount = 0) {
         $quizstats = new stdClass();
         $quizstats->firstattemptscount = $firstattemptscount;
         $quizstats->allattemptscount = $allattemptscount;
 
-        $qstats = new stdClass();
-        $qstats->questions = $questions;
-        $qstats->subquestions = array();
-        $qstats->responses = array();
-
-        return array(0, $quizstats, false);
+        return array(0, $quizstats);
     }
 
     /**
      * Compute the quiz statistics.
      *
-     * @param int $quizid the quiz id.
-     * @param int $currentgroup the current group. 0 for none.
-     * @param bool $nostudentsingroup true if there a no students.
-     * @param bool $useallattempts use all attempts, or just first attempts.
-     * @param array $groupstudents students in this group.
-     * @param array $questions question definitions.
-     * @return array with three elements:
+     * @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 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 array with two elements:
      *      - integer $s Number of attempts included in the stats.
-     *      - array $quizstats The statistics for overall attempt scores.
-     *      - array $qstats The statistics for each question.
+     *      - object $quizstats The statistics for overall attempt scores.
      */
-    protected function compute_stats($quizid, $currentgroup, $nostudentsingroup,
-            $useallattempts, $groupstudents, $questions) {
+    protected function calculate_quiz_stats($quizid, $currentgroup, $useallattempts, $groupstudents, $p, $sumofmarkvariance) {
         global $DB;
 
         // Calculating MEAN of marks for all attempts by students
         // http://docs.moodle.org/dev/Quiz_item_analysis_calculations_in_practise
         //     #Calculating_MEAN_of_grades_for_all_attempts_by_students.
-        if ($nostudentsingroup) {
-            return $this->get_emtpy_stats($questions);
-        }
-
         list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql(
                 $quizid, $currentgroup, $groupstudents, true);
 
@@ -628,7 +615,7 @@ class quiz_statistics_report extends quiz_default_report {
                 GROUP BY CASE WHEN attempt = 1 THEN 1 ELSE 0 END", $qaparams);
 
         if (!$attempttotals) {
-            return $this->get_emtpy_stats($questions);
+            return $this->get_empty_stats();
         }
 
         if (isset($attempttotals[1])) {
@@ -660,10 +647,8 @@ class quiz_statistics_report extends quiz_default_report {
 
         $s = $usingattempts->countrecs;
         if ($s == 0) {
-            return $this->get_emtpy_stats($questions, $firstattempts->countrecs,
-                    $allattempts->countrecs);
+            return $this->get_empty_stats($firstattempts->countrecs, $allattempts->countrecs);
         }
-        $summarksavg = $usingattempts->total / $usingattempts->countrecs;
 
         $quizstats = new stdClass();
         $quizstats->allattempts = $useallattempts;
@@ -726,113 +711,55 @@ class quiz_statistics_report extends quiz_default_report {
                 if ($k2) {
                     $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));
-                if ($k2) {
-                    $quizstats->kurtosis = $k4 / ($k2*$k2);
+                // Kurtosis.
+                if ($s > 3) {
+                    $k4= $s*$s*((($s+1)*$m4)-(3*($s-1)*$m2*$m2))/(($s-1)*($s-2)*($s-3));
+                    if ($k2) {
+                        $quizstats->kurtosis = $k4 / ($k2*$k2);
+                    }
                 }
             }
         }
 
-        $qstats = new quiz_statistics_question_stats($questions, $s, $summarksavg);
-        $qstats->load_step_data($quizid, $currentgroup, $groupstudents, $useallattempts);
-        $qstats->compute_statistics();
-
         if ($s > 1) {
-            $p = count($qstats->questions); // Number of positions.
             if ($p > 1 && isset($k2)) {
                 $quizstats->cic = (100 * $p / ($p -1)) *
-                        (1 - ($qstats->get_sum_of_mark_variance()) / $k2);
+                        (1 - ($sumofmarkvariance / $k2));
                 $quizstats->errorratio = 100 * sqrt(1 - ($quizstats->cic / 100));
                 $quizstats->standarderror = $quizstats->errorratio *
                         $quizstats->standarddeviation / 100;
             }
         }
 
-        return array($s, $quizstats, $qstats);
+        $this->cache_stats(quiz_statistics_qubaids_condition($quizid, $currentgroup, $groupstudents, $useallattempts), $quizstats);
+
+        return array($s, $quizstats);
     }
 
     /**
      * Load the cached statistics from the database.
      *
-     * @param object $quiz the quiz settings
-     * @param int $currentgroup the current group. 0 for none.
-     * @param bool $nostudentsingroup true if there a no students.
-     * @param bool $useallattempts use all attempts, or just first attempts.
-     * @param array $groupstudents students in this group.
-     * @param array $questions question definitions.
-     * @return array with 4 elements:
-     *     - $quizstats The statistics for overall attempt scores.
-     *     - $questions The questions, with an additional _stats field.
-     *     - $subquestions The subquestions, if any, with an additional _stats field.
-     *     - $s Number of attempts included in the stats.
-     * If there is no cached data in the database, returns an array of four nulls.
+     * @param $qubaids qubaid_condition
+     * @return The statistics for overall attempt scores or false if not cached.
      */
-    protected function try_loading_cached_stats($quiz, $currentgroup,
-            $nostudentsingroup, $useallattempts, $groupstudents, $questions) {
+    protected function get_cached_quiz_stats($qubaids) {
         global $DB;
 
         $timemodified = time() - self::TIME_TO_CACHE_STATS;
-        $quizstats = $DB->get_record_select('quiz_statistics',
-                'quizid = ? AND groupid = ? AND allattempts = ? AND timemodified > ?',
-                array($quiz->id, $currentgroup, $useallattempts, $timemodified));
-
-        if (!$quizstats) {
-            // No cached data found.
-            return array(null, $questions, null, null);
-        }
-
-        if ($useallattempts) {
-            $s = $quizstats->allattemptscount;
-        } else {
-            $s = $quizstats->firstattemptscount;
-        }
-
-        $subquestions = array();
-        $questionstats = $DB->get_records('quiz_question_statistics',
-                array('quizstatisticsid' => $quizstats->id));
-
-        $subquestionstats = array();
-        foreach ($questionstats as $stat) {
-            if ($stat->slot) {
-                $questions[$stat->slot]->_stats = $stat;
-            } else {
-                $subquestionstats[$stat->questionid] = $stat;
-            }
-        }
-
-        if (!empty($subquestionstats)) {
-            $subqstofetch = array_keys($subquestionstats);
-            $subquestions = question_load_questions($subqstofetch);
-            foreach ($subquestions as $subqid => $subq) {
-                $subquestions[$subqid]->_stats = $subquestionstats[$subqid];
-                $subquestions[$subqid]->maxmark = $subq->defaultmark;
-            }
-        }
-
-        return array($quizstats, $questions, $subquestions, $s);
+        return  $DB->get_record_select('quiz_statistics', 'hashcode = ? AND timemodified > ?',
+                                       array($qubaids->get_hash_code(), $timemodified));
     }
 
     /**
-     * Store the statistics in the cache tables in the database.
-     *
-     * @param object $quizid the quiz id.
-     * @param int $currentgroup the current group. 0 for none.
-     * @param bool $useallattempts use all attempts, or just first attempts.
-     * @param object $quizstats The statistics for overall attempt scores.
-     * @param array $questions The questions, with an additional _stats field.
-     * @param array $subquestions The subquestions, if any, with an additional _stats field.
+     * @param $qubaids    qubaid_condition
+     * @param $quizstats  object            the quiz stats to cache
      */
-    protected function cache_stats($quizid, $currentgroup,
-            $quizstats, $questions, $subquestions) {
+    protected function cache_stats($qubaids, $quizstats) {
         global $DB;
 
         $toinsert = clone($quizstats);
-        $toinsert->quizid = $quizid;
-        $toinsert->groupid = $currentgroup;
+        $toinsert->hashcode = $qubaids->get_hash_code();
         $toinsert->timemodified = time();
 
         // Fix up some dodgy data.
@@ -844,19 +771,8 @@ class quiz_statistics_report extends quiz_default_report {
         }
 
         // Store the data.
-        $quizstats->id = $DB->insert_record('quiz_statistics', $toinsert);
-
-        foreach ($questions as $question) {
-            $question->_stats->quizstatisticsid = $quizstats->id;
-            $DB->insert_record('quiz_question_statistics', $question->_stats, false);
-        }
-
-        foreach ($subquestions as $subquestion) {
-            $subquestion->_stats->quizstatisticsid = $quizstats->id;
-            $DB->insert_record('quiz_question_statistics', $subquestion->_stats, false);
-        }
+        $DB->insert_record('quiz_statistics', $toinsert);
 
-        return $quizstats->id;
     }
 
     /**
@@ -878,35 +794,45 @@ class quiz_statistics_report extends quiz_default_report {
     protected function get_quiz_and_questions_stats($quiz, $currentgroup,
             $nostudentsingroup, $useallattempts, $groupstudents, $questions) {
 
-        list($quizstats, $questions, $subquestions, $s) =
-                $this->try_loading_cached_stats($quiz, $currentgroup, $nostudentsingroup,
-                        $useallattempts, $groupstudents, $questions);
+        $qubaids = quiz_statistics_qubaids_condition($quiz->id, $currentgroup, $groupstudents, $useallattempts);
 
-        if (is_null($quizstats)) {
-            list($s, $quizstats, $qstats) = $this->compute_stats($quiz->id,
-                    $currentgroup, $nostudentsingroup, $useallattempts, $groupstudents, $questions);
+        $quizstats = $this->get_cached_quiz_stats($qubaids);
 
-            if ($s) {
-                $questions = $qstats->questions;
-                $subquestions = $qstats->subquestions;
+        $qstats = new question_statistics($questions);
 
-                $quizstatisticsid = $this->cache_stats($quiz->id, $currentgroup,
-                        $quizstats, $questions, $subquestions);
+        if (empty($quizstats)) {
+            // Recalculate now.
+            $qstats->calculate($qubaids);
 
-                $this->analyse_responses($quizstatisticsid, $quiz->id, $currentgroup,
-                        $nostudentsingroup, $useallattempts, $groupstudents,
-                        $questions, $subquestions);
+            if ($nostudentsingroup) {
+                list($s, $quizstats) = $this->get_empty_stats();
+            } else {
+                list($s, $quizstats) = $this->calculate_quiz_stats($quiz->id, $currentgroup, $useallattempts,
+                                                           $groupstudents, count($questions), $qstats->get_sum_of_mark_variance());
             }
+
+            $questions = $qstats->questions;
+            $subquestions = $qstats->subquestions;
+
+            if ($s) {
+                $this->calculate_responses_for_all_questions_and_subquestions($qubaids, $questions, $subquestions);
+            }
+        } else {
+            if ($useallattempts) {
+                $s = $quizstats->allattemptscount;
+            } else {
+                $s = $quizstats->firstattemptscount;
+            }
+            $qstats->get_cached($qubaids);
+            $questions = $qstats->questions;
+            $subquestions = $qstats->subquestions;
+
         }
 
         return array($quizstats, $questions, $subquestions, $s);
     }
 
-    protected function analyse_responses($quizstatisticsid, $quizid, $currentgroup,
-            $nostudentsingroup, $useallattempts, $groupstudents, $questions, $subquestions) {
-
-        $qubaids = quiz_statistics_qubaids_condition(
-                $quizid, $currentgroup, $groupstudents, $useallattempts);
+    protected function calculate_responses_for_all_questions_and_subquestions($qubaids, $questions, $subquestions) {
 
         $done = array();
         foreach ($questions as $question) {
@@ -915,9 +841,8 @@ class quiz_statistics_report extends quiz_default_report {
             }
             $done[$question->id] = 1;
 
-            $responesstats = new quiz_statistics_response_analyser($question);
-            $responesstats->analyse($qubaids);
-            $responesstats->store_cached($quizstatisticsid);
+            $responesstats = new question_response_analyser($question);
+            $responesstats->calculate($qubaids);
         }
 
         foreach ($subquestions as $question) {
@@ -927,9 +852,8 @@ class quiz_statistics_report extends quiz_default_report {
             }
             $done[$question->id] = 1;
 
-            $responesstats = new quiz_statistics_response_analyser($question);
-            $responesstats->analyse($qubaids);
-            $responesstats->store_cached($quizstatisticsid);
+            $responesstats = new question_response_analyser($question);
+            $responesstats->calculate($qubaids);
         }
     }
 
@@ -957,12 +881,13 @@ class quiz_statistics_report extends quiz_default_report {
     /**
      * Generate the snipped of HTML that says when the stats were last caculated,
      * 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 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 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.
      */
@@ -1008,28 +933,13 @@ class quiz_statistics_report extends quiz_default_report {
     /**
      * Clear the cached data for a particular report configuration. This will
      * trigger a re-computation the next time the report is displayed.
-     * @param int $quizid the quiz id.
-     * @param int $currentgroup a group id, or 0.
-     * @param bool $useallattempts whether all attempts, or just first attempts are included.
+     * @param $qubaids qubaid_condition
      */
-    protected function clear_cached_data($quizid, $currentgroup, $useallattempts) {
+    protected function clear_cached_data($qubaids) {
         global $DB;
-
-        $todelete = $DB->get_records_menu('quiz_statistics', array('quizid' => $quizid,
-                'groupid' => $currentgroup, 'allattempts' => $useallattempts), '', 'id, 1');
-
-        if (!$todelete) {
-            return;
-        }
-
-        list($todeletesql, $todeleteparams) = $DB->get_in_or_equal(array_keys($todelete));
-
-        $DB->delete_records_select('quiz_question_statistics',
-                'quizstatisticsid ' . $todeletesql, $todeleteparams);
-        $DB->delete_records_select('quiz_question_response_stats',
-                'quizstatisticsid ' . $todeletesql, $todeleteparams);
-        $DB->delete_records_select('quiz_statistics',
-                'id ' . $todeletesql, $todeleteparams);
+        $DB->delete_records('quiz_statistics', array('hashcode' => $qubaids->get_hash_code()));
+        $DB->delete_records('question_statistics', array('hashcode' => $qubaids->get_hash_code()));
+        $DB->delete_records('question_response_analysis', array('hashcode' => $qubaids->get_hash_code()));
     }
 
     /**
@@ -1067,42 +977,3 @@ class quiz_statistics_report extends quiz_default_report {
     }
 }
 
-function quiz_statistics_attempts_sql($quizid, $currentgroup, $groupstudents,
-        $allattempts = true, $includeungraded = false) {
-    global $DB;
-
-    $fromqa = '{quiz_attempts} quiza ';
-
-    $whereqa = 'quiza.quiz = :quizid AND quiza.preview = 0 AND quiza.state = :quizstatefinished';
-    $qaparams = array('quizid' => $quizid, 'quizstatefinished' => quiz_attempt::FINISHED);
-
-    if (!empty($currentgroup) && $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';
-    }
-
-    if (!$includeungraded) {
-        $whereqa .= ' AND quiza.sumgrades IS NOT NULL';
-    }
-
-    return array($fromqa, $whereqa, $qaparams);
-}
-
-/**
- * Return a {@link qubaid_condition} from the values returned by
- * {@link quiz_statistics_attempts_sql}
- * @param string $fromqa from quiz_statistics_attempts_sql.
- * @param string $whereqa from quiz_statistics_attempts_sql.
- */
-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);
-    return new qubaid_join($fromqa, 'quiza.uniqueid', $whereqa, $qaparams);
-}
index c7d7852..be6c7a9 100644 (file)
@@ -33,30 +33,14 @@ require_once(dirname(__FILE__) . '/../../../../config.php');
 require_once($CFG->libdir . '/graphlib.php');
 require_once($CFG->dirroot . '/mod/quiz/locallib.php');
 require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php');
-
-
-/**
- * This helper function returns a sequence of colours each time it is called.
- * Used for chooseing colours for graph data series.
- * @return string colour name.
- */
-function graph_get_new_colour() {
-    static $colourindex = -1;
-    $colours = array('red', 'green', 'yellow', 'orange', 'purple', 'black',
-            'maroon', 'blue', 'ltgreen', 'navy', 'ltred', 'ltltgreen', 'ltltorange',
-            'olive', 'gray', 'ltltred', 'ltorange', 'lime', 'ltblue', 'ltltblue');
-
-    $colourindex = ($colourindex + 1) % count($colours);
-
-    return $colours[$colourindex];
-}
+require_once($CFG->dirroot . '/mod/quiz/report/statistics/statisticslib.php');
 
 // Get the parameters.
-$quizstatisticsid = required_param('id', PARAM_INT);
+$quizid = required_param('quizid', PARAM_INT);
+$currentgroup = required_param('currentgroup', PARAM_INT);
+$useallattempts = required_param('useallattempts', PARAM_INT);
 
-// Load enough data to check permissions.
-$quizstatistics = $DB->get_record('quiz_statistics', array('id' => $quizstatisticsid));
-$quiz = $DB->get_record('quiz', array('id' => $quizstatistics->quizid), '*', MUST_EXIST);
+$quiz = $DB->get_record('quiz', array('id' => $quizid), '*', MUST_EXIST);
 $cm = get_coursemodule_from_instance('quiz', $quiz->id);
 
 // Check access.
@@ -69,14 +53,21 @@ if (groups_get_activity_groupmode($cm)) {
 } else {
     $groups = array();
 }
-if ($quizstatistics->groupid && !in_array($quizstatistics->groupid, array_keys($groups))) {
+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);
 
 // Load the rest of the required data.
 $questions = quiz_report_get_significant_questions($quiz);
-$questionstatistics = $DB->get_records_select('quiz_question_statistics',
-        'quizstatisticsid = ? AND slot IS NOT NULL', array($quizstatistics->id));
+
+// Load enough data to check permissions.
+$quizstatistics = $DB->get_record('quiz_statistics', array('hashcode' => $qubaids->get_hash_code()));
+$questionstatistics = $DB->get_records_select('question_statistics', 'hashcode = ? AND slot IS NOT NULL',
+                                              array($qubaids->get_hash_code()));
 
 // Create the graph, and set the basic options.
 $graph = new graph(800, 600);
@@ -108,7 +99,7 @@ $xdata = array();
 foreach (array_keys($fieldstoplot) as $fieldtoplot) {
     $ydata[$fieldtoplot] = array();
     $graph->y_format[$fieldtoplot] = array(
-        'colour' => graph_get_new_colour(),
+        'colour' => quiz_statistics_graph_get_new_colour(),
         'bar' => 'fill',
         'shadow_offset' => 1,
         'legend' => $fieldstoplot[$fieldtoplot]
index d50b0d4..508775b 100644 (file)
@@ -61,7 +61,7 @@ class quiz_statistics_question_table extends flexible_table {
      * @param bool $hassubqs
      */
     public function question_setup($reporturl, $questiondata,
-            quiz_statistics_response_analyser $responesstats) {
+            question_response_analyser $responesstats) {
         $this->questiondata = $questiondata;
 
         $this->define_baseurl($reporturl->out());
diff --git a/mod/quiz/report/statistics/statisticslib.php b/mod/quiz/report/statistics/statisticslib.php
new file mode 100644 (file)
index 0000000..c947dc8
--- /dev/null
@@ -0,0 +1,84 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Common functions for the quiz statistics report.
+ *
+ * @package    quiz_statistics
+ * @copyright  2013 The Open University
+ * @author     James Pratt me@jamiep.org
+ * @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) {
+    global $DB;
+
+    $fromqa = '{quiz_attempts} quiza ';
+
+    $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) {
+        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';
+    }
+
+    if (!$includeungraded) {
+        $whereqa .= ' AND quiza.sumgrades IS NOT NULL';
+    }
+
+    return array($fromqa, $whereqa, $qaparams);
+}
+
+/**
+ * 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 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);
+    return new qubaid_join($fromqa, 'quiza.uniqueid', $whereqa, $qaparams);
+}
+
+/**
+ * This helper function returns a sequence of colours each time it is called.
+ * Used for choosing colours for graph data series.
+ * @return string colour name.
+ */
+function quiz_statistics_graph_get_new_colour() {
+    static $colourindex = -1;
+    $colours = array('red', 'green', 'yellow', 'orange', 'purple', 'black',
+        'maroon', 'blue', 'ltgreen', 'navy', 'ltred', 'ltltgreen', 'ltltorange',
+        'olive', 'gray', 'ltltred', 'ltorange', 'lime', 'ltblue', 'ltltblue');
+
+    $colourindex = ($colourindex + 1) % count($colours);
+
+    return $colours[$colourindex];
+}
index 1a7db85..db191e4 100644 (file)
@@ -15,7 +15,7 @@
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * Unit tests for (some of) mod/quiz/report/statistics/qstats.php.
+ * Unit tests for (some of) /question/engine/statistics.php
  *
  * @package   quiz_statistics
  * @category  phpunit
@@ -28,18 +28,18 @@ defined('MOODLE_INTERNAL') || die();
 
 global $CFG;
 require_once($CFG->libdir . '/questionlib.php');
-require_once($CFG->dirroot . '/mod/quiz/report/statistics/qstats.php');
+require_once($CFG->dirroot . '/question/engine/statistics.php');
 require_once($CFG->dirroot . '/mod/quiz/locallib.php');
 require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php');
 
 
 /**
- * Test helper subclass of quiz_statistics_question_stats
+ * Test helper subclass of question_statistics
  *
  * @copyright 2010 The Open University
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class testable_quiz_statistics_question_stats extends quiz_statistics_question_stats {
+class testable_question_statistics extends question_statistics {
     public function set_step_data($states) {
         $this->lateststeps = $states;
     }
@@ -47,11 +47,38 @@ class testable_quiz_statistics_question_stats extends quiz_statistics_question_s
     protected function get_random_guess_score($questiondata) {
         return 0;
     }
-}
 
+    /**
+     * @param $qubaids qubaid_condition is ignored in this test
+     * @return array with three items
+     *              - $lateststeps array of latest step data for the question usages
+     *              - $summarks    array of total marks for each usage, indexed by usage id
+     *              - $summarksavg the average of the total marks over all the usages     */
+    protected function get_latest_steps($qubaids) {
+        $summarks = array();
+        $fakeusageid = 0;
+        foreach ($this->lateststeps as $step) {
+            // The same 'sumgrades' field is available in step data for every slot, we will ignore all slots but slot 1.
+            // The step for slot 1 is always the first one in the csv file for each usage, we will use that to separate steps from
+            // each usage.
+            if ($step->slot == 1) {
+                $fakeusageid++;
+                $summarks[$fakeusageid] = $step->sumgrades;
+            }
+            unset($step->sumgrades);
+            $step->questionusageid = $fakeusageid;
+        }
 
+        $summarksavg = array_sum($summarks) / count($summarks);
+        return array($this->lateststeps, $summarks, $summarksavg);
+    }
+
+    protected function cache_stats($qubaids) {
+        // No caching wanted for tests.
+    }
+}
 /**
- * Unit tests for (some of) quiz_statistics_question_stats.
+ * Unit tests for (some of) question_statistics.
  *
  * @copyright 2008 Jamie Pratt
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
@@ -68,9 +95,9 @@ class quiz_statistics_question_stats_testcase extends basic_testcase {
         // Data is taken from questions mostly generated by
         // contrib/tools/generators/generator.php.
         $questions = $this->get_records_from_csv(__DIR__.'/fixtures/mdl_question.csv');
-        $this->qstats = new testable_quiz_statistics_question_stats($questions, 22, 10045.45455);
+        $this->qstats = new testable_question_statistics($questions, 22, 10045.45455);
         $this->qstats->set_step_data($steps);
-        $this->qstats->compute_statistics();
+        $this->qstats->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,
index 50169d6..6e816ca 100644 (file)
@@ -17,7 +17,7 @@
 /**
  * Quiz attempt walk through using data from csv file.
  *
- * @package    mod_quiz
+ * @package    quiz_statistics
  * @category   phpunit
  * @copyright  2013 The Open University
  * @author     Jamie Pratt <me@jamiep.org>
@@ -42,7 +42,8 @@ class testable_quiz_statistics_report extends quiz_statistics_report {
 
     public function get_stats($quiz, $useallattempts = true,
                               $currentgroup = 0, $groupstudents = array(), $nostudentsingroup = false) {
-        $this->clear_cached_data($quiz->id, $currentgroup, $useallattempts);
+        $qubaids = quiz_statistics_qubaids_condition($quiz->id, $currentgroup, $groupstudents, $useallattempts);
+        $this->clear_cached_data($qubaids);
         $questions = $this->load_and_initialise_questions_for_calculations($quiz);
         return $this->get_quiz_and_questions_stats($quiz, $currentgroup, $nostudentsingroup,
                                                    $useallattempts, $groupstudents, $questions);
@@ -52,7 +53,7 @@ class testable_quiz_statistics_report extends quiz_statistics_report {
 /**
  * Quiz attempt walk through using data from csv file.
  *
- * @package    mod_quiz
+ * @package    quiz_statistics
  * @category   phpunit
  * @copyright  2013 The Open University
  * @author     Jamie Pratt <me@jamiep.org>
index 1f1d44e..8151693 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2013050100;
-$plugin->requires  = 2013050100;
+$plugin->version   = 2013092000;
+$plugin->requires  = 2013092000;
 $plugin->cron      = 18000;
 $plugin->component = 'quiz_statistics';
index 38b8f3d..71a08b0 100644 (file)
@@ -421,6 +421,10 @@ abstract class question_bank {
         // Delete any old question preview that got left in the database.
         require_once($CFG->dirroot . '/question/previewlib.php');
         question_preview_cron();
+
+        // Clear older calculated stats from cache.
+        require_once($CFG->dirroot . '/question/engine/statisticslib.php');
+        question_usage_statistics_cron();
     }
 }
 
index e52b80f..68e3bc4 100644 (file)
@@ -359,16 +359,16 @@ ORDER BY
      * Load information about the latest state of each question from the database.
      *
      * @param qubaid_condition $qubaids used to restrict which usages are included
-     * in the query. See {@link qubaid_condition}.
-     * @param array $slots A list of slots for the questions you want to konw about.
+     *                                  in the query. See {@link qubaid_condition}.
+     * @param array            $slots   A list of slots for the questions you want to konw about.
+     * @param string|null      $fields
      * @return array of records. See the SQL in this function to see the fields available.
      */
-    public function load_questions_usages_latest_steps(qubaid_condition $qubaids, $slots) {
+    public function load_questions_usages_latest_steps(qubaid_condition $qubaids, $slots, $fields = null) {
         list($slottest, $params) = $this->db->get_in_or_equal($slots, SQL_PARAMS_NAMED, 'slot');
 
-        $records = $this->db->get_records_sql("
-SELECT
-    qas.id,
+        if ($fields === null) {
+            $fields =  "qas.id,
     qa.id AS questionattemptid,
     qa.questionusageid,
     qa.slot,
@@ -387,7 +387,13 @@ SELECT
     qas.state,
     qas.fraction,
     qas.timecreated,
-    qas.userid
+    qas.userid";
+
+        }
+
+        $records = $this->db->get_records_sql("
+SELECT
+    {$fields}
 
 FROM {$qubaids->from_question_attempts('qa')}
 JOIN {question_attempt_steps} qas ON
@@ -1458,6 +1464,14 @@ abstract class qubaid_condition {
      * @return the params needed by a query that uses {@link usage_id_in()}.
      */
     public abstract function usage_id_in_params();
+
+    /**
+     * @return string 40-character hash code that uniquely identifies the combination of properties and class name of this qubaid
+     *                  condition.
+     */
+    public function get_hash_code() {
+        return sha1(serialize($this));
+    }
 }
 
 
similarity index 82%
rename from mod/quiz/report/statistics/responseanalysis.php
rename to question/engine/responseanalysis.php
index 332555a..0118dfd 100644 (file)
  * This file contains the code to analyse all the responses to a particular
  * question.
  *
- * @package   quiz_statistics
- * @copyright 2010 The Open University
- * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @package    core
+ * @subpackage questionbank
+ * @copyright  2013 Open University
+ * @author     Jamie Pratt <me@jamiep.org>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
 
@@ -31,13 +33,13 @@ defined('MOODLE_INTERNAL') || die();
  * This class can store and compute the analysis of the responses to a particular
  * question.
  *
- * @copyright 2010 The Open University
+ * @copyright 2013 Open University
+ * @author    Jamie Pratt <me@jamiep.org>
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class quiz_statistics_response_analyser {
+class question_response_analyser {
     /** @var object the data from the database that defines the question. */
     protected $questiondata;
-    protected $loaded = false;
 
     /**
      * @var array This is a multi-dimensional array that stores the results of
@@ -119,9 +121,9 @@ class quiz_statistics_response_analyser {
     /**
      * Analyse all the response data for for all the specified attempts at
      * this question.
-     * @param $qubaids which attempts to consider.
+     * @param qubaid_condition $qubaids which attempts to consider.
      */
-    public function analyse($qubaids) {
+    public function calculate($qubaids) {
         // Load data.
         $dm = new question_engine_data_mapper();
         $questionattempts = $dm->load_attempts_at_question($this->questiondata->id, $qubaids);
@@ -130,8 +132,8 @@ class quiz_statistics_response_analyser {
         foreach ($questionattempts as $qa) {
             $this->add_data_from_one_attempt($qa);
         }
+        $this->store_cached($qubaids);
 
-        $this->loaded = true;
     }
 
     /**
@@ -164,19 +166,16 @@ class quiz_statistics_response_analyser {
     }
 
     /**
-     * Store the computed response analysis in the quiz_question_response_stats
-     * table.
-     * @param int $quizstatisticsid the cached quiz statistics to load the
+     * Store the computed response analysis in the question_response_analysis table.
+     * @param qubaid_condition $qubaids
      * data corresponding to.
-     * @return bool true if cached data was found in the database and loaded,
-     * otherwise false, to mean no data was loaded.
+     * @return bool true if cached data was found in the database and loaded, otherwise false, to mean no data was loaded.
      */
-    public function load_cached($quizstatisticsid) {
+    public function load_cached($qubaids) {
         global $DB;
 
-        $rows = $DB->get_records('quiz_question_response_stats',
-                array('quizstatisticsid' => $quizstatisticsid,
-                        'questionid' => $this->questiondata->id));
+        $rows = $DB->get_records('question_response_analysis',
+                array('hashcode' => $qubaids->get_hash_code(), 'questionid' => $this->questiondata->id));
         if (!$rows) {
             return false;
         }
@@ -186,28 +185,22 @@ class quiz_statistics_response_analyser {
             $this->responses[$row->subqid][$row->aid][$row->response]->count = $row->rcount;
             $this->responses[$row->subqid][$row->aid][$row->response]->fraction = $row->credit;
         }
-        $this->loaded = true;
         return true;
     }
 
     /**
-     * Store the computed response analysis in the quiz_question_response_stats
-     * table.
-     * @param int $quizstatisticsid the cached quiz statistics this correspons to.
+     * Store the computed response analysis in the question_response_analysis table.
+     * @param qubaid_condition $qubaids
      */
-    public function store_cached($quizstatisticsid) {
+    public function store_cached($qubaids) {
         global $DB;
 
-        if (!$this->loaded) {
-            throw new coding_exception(
-                    'Question responses have not been analyised. Cannot store in the database.');
-        }
-
+        $cachetime = time();
         foreach ($this->responses as $subpartid => $partdata) {
             foreach ($partdata as $responseclassid => $classdata) {
                 foreach ($classdata as $response => $data) {
                     $row = new stdClass();
-                    $row->quizstatisticsid = $quizstatisticsid;
+                    $row->hashcode = $qubaids->get_hash_code();
                     $row->questionid = $this->questiondata->id;
                     $row->subqid = $subpartid;
                     if ($responseclassid === '') {
@@ -218,7 +211,8 @@ class quiz_statistics_response_analyser {
                     $row->response = $response;
                     $row->rcount = $data->count;
                     $row->credit = $data->fraction;
-                    $DB->insert_record('quiz_question_response_stats', $row, false);
+                    $row->timemodified = $cachetime;
+                    $DB->insert_record('question_response_analysis', $row, false);
                 }
             }
         }
diff --git a/question/engine/statistics.php b/question/engine/statistics.php
new file mode 100644 (file)
index 0000000..e5f4d12
--- /dev/null
@@ -0,0 +1,447 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Question statistics calculations class. Used in the quiz statistics report but also available for use elsewhere.
+ *
+ * @package    core
+ * @subpackage questionbank
+ * @copyright  2013 Open University
+ * @author     Jamie Pratt <me@jamiep.org>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+
+/**
+ * This class has methods to compute the question statistics from the raw data.
+ *
+ * @copyright 2013 Open University
+ * @author    Jamie Pratt <me@jamiep.org>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class question_statistics {
+    public $questions;
+    public $subquestions = array();
+
+    protected $summarksavg;
+
+    protected $sumofmarkvariance = 0;
+    protected $randomselectors = array();
+
+    /**
+     * Constructor.
+     *
+     * @param $questions array the main questions indexed by slot.
+     */
+    public function __construct($questions) {
+        foreach ($questions as $slot => $question) {
+            $question->_stats = $this->make_blank_question_stats();
+            $question->_stats->questionid = $question->id;
+            $question->_stats->slot = $slot;
+        }
+
+        $this->questions = $questions;
+    }
+
+    /**
+     * @return object ready to hold all the question statistics.
+     */
+    protected function make_blank_question_stats() {
+        $stats = new stdClass();
+        $stats->slot = null;
+        $stats->s = 0;
+        $stats->totalmarks = 0;
+        $stats->totalothermarks = 0;
+        $stats->markvariancesum = 0;
+        $stats->othermarkvariancesum = 0;
+        $stats->covariancesum = 0;
+        $stats->covariancemaxsum = 0;
+        $stats->subquestion = false;
+        $stats->subquestions = '';
+        $stats->covariancewithoverallmarksum = 0;
+        $stats->randomguessscore = null;
+        $stats->markarray = array();
+        $stats->othermarksarray = array();
+        return $stats;
+    }
+
+    /**
+     * @param $qubaids qubaid_condition
+     * @return array with three items
+     *              - $lateststeps array of latest step data for the question usages
+     *              - $summarks    array of total marks for each usage, indexed by usage id
+     *              - $summarksavg the average of the total marks over all the usages
+     */
+    protected function get_latest_steps($qubaids) {
+        $dm = new question_engine_data_mapper();
+
+        $fields = "    qas.id,
+    qa.questionusageid,
+    qa.questionid,
+    qa.slot,
+    qa.maxmark,
+    qas.fraction * qa.maxmark as mark";
+
+        $lateststeps = $dm->load_questions_usages_latest_steps($qubaids, array_keys($this->questions), $fields);
+        $summarks = array();
+        if ($lateststeps) {
+            foreach ($lateststeps as $step) {
+                if (!isset($summarks[$step->questionusageid])) {
+                    $summarks[$step->questionusageid] = 0;
+                }
+                $summarks[$step->questionusageid] += $step->mark;
+            }
+            $summarksavg = array_sum($summarks) / count($summarks);
+        } else {
+            $summarksavg = null;
+        }
+
+        return array($lateststeps, $summarks, $summarksavg);
+    }
+
+    /**
+     * @param $qubaids qubaid_condition
+     */
+    public function calculate($qubaids) {
+        set_time_limit(0);
+
+        list($lateststeps, $summarks, $summarksavg) = $this->get_latest_steps($qubaids);
+
+        if ($lateststeps) {
+            $subquestionstats = array();
+
+            // Compute the statistics of position, and for random questions, work
+            // out which questions appear in which positions.
+            foreach ($lateststeps as $step) {
+                $this->initial_steps_walker($step, $this->questions[$step->slot]->_stats, $summarks);
+
+                // If this is a random question what is the real item being used?
+                if ($step->questionid != $this->questions[$step->slot]->id) {
+                    if (!isset($subquestionstats[$step->questionid])) {
+                        $subquestionstats[$step->questionid] = $this->make_blank_question_stats();
+                        $subquestionstats[$step->questionid]->questionid = $step->questionid;
+                        $subquestionstats[$step->questionid]->usedin = array();
+                        $subquestionstats[$step->questionid]->subquestion = true;
+                        $subquestionstats[$step->questionid]->differentweights = false;
+                        $subquestionstats[$step->questionid]->maxmark = $step->maxmark;
+                    } else if ($subquestionstats[$step->questionid]->maxmark != $step->maxmark) {
+                        $subquestionstats[$step->questionid]->differentweights = true;
+                    }
+
+                    $this->initial_steps_walker($step, $subquestionstats[$step->questionid], $summarks, false);
+
+                    $number = $this->questions[$step->slot]->number;
+                    $subquestionstats[$step->questionid]->usedin[$number] = $number;
+
+                    $randomselectorstring = $this->questions[$step->slot]->category .
+                        '/' . $this->questions[$step->slot]->questiontext;
+                    if (!isset($this->randomselectors[$randomselectorstring])) {
+                        $this->randomselectors[$randomselectorstring] = array();
+                    }
+                    $this->randomselectors[$randomselectorstring][$step->questionid] =
+                        $step->questionid;
+                }
+            }
+
+            foreach ($this->randomselectors as $key => $notused) {
+                ksort($this->randomselectors[$key]);
+            }
+
+            // Compute the statistics of question id, if we need any.
+            $this->subquestions = question_load_questions(array_keys($subquestionstats));
+            foreach ($this->subquestions as $qid => $subquestion) {
+                $subquestion->_stats = $subquestionstats[$qid];
+                $subquestion->maxmark = $subquestion->_stats->maxmark;
+                $subquestion->_stats->randomguessscore = $this->get_random_guess_score($subquestion);
+
+                $this->initial_question_walker($subquestion->_stats);
+
+                if ($subquestionstats[$qid]->differentweights) {
+                    // TODO output here really sucks, but throwing is too severe.
+                    global $OUTPUT;
+                    echo $OUTPUT->notification(
+                        get_string('erroritemappearsmorethanoncewithdifferentweight',
+                                   'quiz_statistics', $this->subquestions[$qid]->name));
+                }
+
+                if ($subquestion->_stats->usedin) {
+                    sort($subquestion->_stats->usedin, SORT_NUMERIC);
+                    $subquestion->_stats->positions = implode(',', $subquestion->_stats->usedin);
+                } else {
+                    $subquestion->_stats->positions = '';
+                }
+            }
+
+            // Finish computing the averages, and put the subquestion 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->questions);
+            while (list($slot, $question) = each($this->questions)) {
+                $nextquestion = current($this->questions);
+                $question->_stats->positions = $question->number;
+                $question->_stats->maxmark = $question->maxmark;
+                $question->_stats->randomguessscore = $this->get_random_guess_score($question);
+
+                $this->initial_question_walker($question->_stats);
+
+                if ($question->qtype == 'random') {
+                    $randomselectorstring = $question->category.'/'.$question->questiontext;
+                    if ($nextquestion && $nextquestion->qtype == 'random') {
+                        $nextrandomselectorstring = $nextquestion->category . '/' .
+                            $nextquestion->questiontext;
+                        if ($randomselectorstring == $nextrandomselectorstring) {
+                            continue; // Next loop iteration.
+                        }
+                    }
+                    if (isset($this->randomselectors[$randomselectorstring])) {
+                        $question->_stats->subquestions = implode(',',
+                                                                  $this->randomselectors[$randomselectorstring]);
+                    }
+                }
+            }
+
+            // Go through the records one more time.
+            foreach ($lateststeps as $step) {
+                $this->secondary_steps_walker($step, $this->questions[$step->slot]->_stats, $summarks, $summarksavg);
+
+                if ($this->questions[$step->slot]->qtype == 'random') {
+                    $this->secondary_steps_walker($step, $this->subquestions[$step->questionid]->_stats, $summarks, $summarksavg);
+                }
+            }
+
+            $sumofcovariancewithoverallmark = 0;
+            foreach ($this->questions as $slot => $question) {
+                $this->secondary_question_walker($question->_stats);
+
+                $this->sumofmarkvariance += $question->_stats->markvariance;
+
+                if ($question->_stats->covariancewithoverallmark >= 0) {
+                    $sumofcovariancewithoverallmark +=
+                        sqrt($question->_stats->covariancewithoverallmark);
+                    $question->_stats->negcovar = 0;
+                } else {
+                    $question->_stats->negcovar = 1;
+                }
+            }
+
+            foreach ($this->subquestions as $subquestion) {
+                $this->secondary_question_walker($subquestion->_stats);
+            }
+
+            foreach ($this->questions as $question) {
+                if ($sumofcovariancewithoverallmark) {
+                    if ($question->_stats->negcovar) {
+                        $question->_stats->effectiveweight = null;
+                    } else {
+                        $question->_stats->effectiveweight = 100 *
+                            sqrt($question->_stats->covariancewithoverallmark) /
+                            $sumofcovariancewithoverallmark;
+                    }
+                } else {
+                    $question->_stats->effectiveweight = null;
+                }
+            }
+            $this->cache_stats($qubaids);
+        }
+
+
+    }
+
+    /**
+     * @param $qubaids qubaid_condition
+     */
+    protected function cache_stats($qubaids) {
+        global $DB;
+        $cachetime = time();
+        foreach ($this->questions as $question) {
+            $question->_stats->hashcode = $qubaids->get_hash_code();
+            $question->_stats->timemodified = $cachetime;
+            $DB->insert_record('question_statistics', $question->_stats, false);
+        }
+
+        foreach ($this->subquestions as $subquestion) {
+            $subquestion->_stats->hashcode = $qubaids->get_hash_code();
+            $subquestion->_stats->timemodified = $cachetime;
+            $DB->insert_record('question_statistics', $subquestion->_stats, false);
+        }
+
+    }
+
+    /**
+     * Update $stats->totalmarks, $stats->markarray, $stats->totalothermarks
+     * and $stats->othermarksarray to include another state.
+     *
+     * @param object $step the state to add to the statistics.
+     * @param object $stats the question statistics we are accumulating.
+     * @param array  $summarks of the sum of marks for each question usage, indexed by question usage id
+     * @param bool $positionstat whether this is a statistic of position of question.
+     */
+    protected function initial_steps_walker($step, $stats, $summarks, $positionstat = true) {
+        $stats->s++;
+        $stats->totalmarks += $step->mark;
+        $stats->markarray[] = $step->mark;
+
+        if ($positionstat) {
+            $stats->totalothermarks += $summarks[$step->questionusageid] - $step->mark;
+            $stats->othermarksarray[] = $summarks[$step->questionusageid] - $step->mark;
+
+        } else {
+            $stats->totalothermarks += $summarks[$step->questionusageid];
+            $stats->othermarksarray[] = $summarks[$step->questionusageid];
+        }
+    }
+
+    /**
+     * Perform some computations on the per-question statistics calculations after
+     * we have been through all the states.
+     *
+     * @param object $stats quetsion stats to update.
+     */
+    protected function initial_question_walker($stats) {
+        $stats->markaverage = $stats->totalmarks / $stats->s;
+
+        if ($stats->maxmark != 0) {
+            $stats->facility = $stats->markaverage / $stats->maxmark;
+        } else {
+            $stats->facility = null;
+        }
+
+        $stats->othermarkaverage = $stats->totalothermarks / $stats->s;
+
+        sort($stats->markarray, SORT_NUMERIC);
+        sort($stats->othermarksarray, SORT_NUMERIC);
+    }
+
+    /**
+     * Now we know the averages, accumulate the date needed to compute the higher
+     * moments of the question scores.
+     *
+     * @param object $step     the state to add to the statistics.
+     * @param object $stats    the question statistics we are accumulating.
+     * @param array  $summarks of the sum of marks for each question usage, indexed by question usage id
+     * @param float  $summarksavg the average sum of marks for all question usages
+     */
+    protected function secondary_steps_walker($step, $stats, $summarks, $summarksavg) {
+        $markdifference = $step->mark - $stats->markaverage;
+        if ($stats->subquestion) {
+            $othermarkdifference = $summarks[$step->questionusageid] - $stats->othermarkaverage;
+        } else {
+            $othermarkdifference = $summarks[$step->questionusageid] - $step->mark -
+                    $stats->othermarkaverage;
+        }
+        $overallmarkdifference = $summarks[$step->questionusageid] - $summarksavg;
+
+        $sortedmarkdifference = array_shift($stats->markarray) - $stats->markaverage;
+        $sortedothermarkdifference = array_shift($stats->othermarksarray) -
+                $stats->othermarkaverage;
+
+        $stats->markvariancesum += pow($markdifference, 2);
+        $stats->othermarkvariancesum += pow($othermarkdifference, 2);
+        $stats->covariancesum += $markdifference * $othermarkdifference;
+        $stats->covariancemaxsum += $sortedmarkdifference * $sortedothermarkdifference;
+        $stats->covariancewithoverallmarksum += $markdifference * $overallmarkdifference;
+    }
+
+    /**
+     * Perform more per-question statistics calculations.
+     *
+     * @param object $stats quetsion stats to update.
+     */
+    protected function secondary_question_walker($stats) {
+        if ($stats->s > 1) {
+            $stats->markvariance = $stats->markvariancesum / ($stats->s - 1);
+            $stats->othermarkvariance = $stats->othermarkvariancesum / ($stats->s - 1);
+            $stats->covariance = $stats->covariancesum / ($stats->s - 1);
+            $stats->covariancemax = $stats->covariancemaxsum / ($stats->s - 1);
+            $stats->covariancewithoverallmark = $stats->covariancewithoverallmarksum /
+                    ($stats->s - 1);
+            $stats->sd = sqrt($stats->markvariancesum / ($stats->s - 1));
+
+        } else {
+            $stats->markvariance = null;
+            $stats->othermarkvariance = null;
+            $stats->covariance = null;
+            $stats->covariancemax = null;
+            $stats->covariancewithoverallmark = null;
+            $stats->sd = null;
+        }
+
+        if ($stats->markvariance * $stats->othermarkvariance) {
+            $stats->discriminationindex = 100 * $stats->covariance /
+                    sqrt($stats->markvariance * $stats->othermarkvariance);
+        } else {
+            $stats->discriminationindex = null;
+        }
+
+        if ($stats->covariancemax) {
+            $stats->discriminativeefficiency = 100 * $stats->covariance /
+                    $stats->covariancemax;
+        } else {
+            $stats->discriminativeefficiency = null;
+        }
+    }
+
+    /**
+     * @param object $questiondata
+     * @return number the random guess score for this question.
+     */
+    protected function get_random_guess_score($questiondata) {
+        return question_bank::get_qtype(
+                $questiondata->qtype, false)->get_random_guess_score($questiondata);
+    }
+
+    /**
+     * Used when computing CIC.
+     * @return number
+     */
+    public function get_sum_of_mark_variance() {
+        return $this->sumofmarkvariance;
+    }
+
+    /**
+     * @param qubaid_condition $qubaids
+     */
+    public function get_cached($qubaids) {
+        global $DB;
+        $questionstats = $DB->get_records('question_statistics',
+                                          array('hashcode' => $qubaids->get_hash_code()));
+
+        $subquestionstats = array();
+        foreach ($questionstats as $stat) {
+            if ($stat->slot) {
+                $this->questions[$stat->slot]->_stats = $stat;
+            } else {
+                $subquestionstats[$stat->questionid] = $stat;
+            }
+        }
+
+        if (!empty($subquestionstats)) {
+            $subqstofetch = array_keys($subquestionstats);
+            $this->subquestions = question_load_questions($subqstofetch);
+            foreach ($this->subquestions as $subqid => $subq) {
+                $this->subquestions[$subqid]->_stats = $subquestionstats[$subqid];
+                $this->subquestions[$subqid]->maxmark = $subq->defaultmark;
+            }
+        }
+    }
+
+}
diff --git a/question/engine/statisticslib.php b/question/engine/statisticslib.php
new file mode 100644 (file)
index 0000000..ab99fc0
--- /dev/null
@@ -0,0 +1,46 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Functions common to the question usage statistics code.
+ *
+ * @package    moodlecore
+ * @subpackage questionbank
+ * @copyright  2013 The Open University
+ * @author     Jamie Pratt <me@jamiep.org>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+
+/**
+ * Question statistics cron code. Deletes cached stats more than a certain age.
+ */
+function question_usage_statistics_cron() {
+    global $DB;
+
+    $expiretime = time() - 5*HOURSECS;
+
+    mtrace("\n  Cleaning up old question statistics cache records...", '');
+
+    $DB->delete_records_select('question_statistics', 'timemodified < ?', array($expiretime));
+    $DB->delete_records_select('question_response_analysis', 'timemodified < ?', array($expiretime));
+
+    mtrace('done.');
+    return true;
+}
index 9474abd..9743c2f 100644 (file)
@@ -29,7 +29,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2013092000.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2013092000.01;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.