]> git.mjollnir.org Git - moodle.git/commitdiff
MDL-14202 "Replace Item Analysis Report with new improved 'Statistics' report." finis...
authorjamiesensei <jamiesensei>
Thu, 11 Sep 2008 12:48:08 +0000 (12:48 +0000)
committerjamiesensei <jamiesensei>
Thu, 11 Sep 2008 12:48:08 +0000 (12:48 +0000)
18 files changed:
lang/en_utf8/quiz_statistics.php
lib/tablelib.php
mod/quiz/report/reportlib.php
mod/quiz/report/statistics/db/install.xml
mod/quiz/report/statistics/db/upgrade.php
mod/quiz/report/statistics/qstats.php
mod/quiz/report/statistics/report.php
mod/quiz/report/statistics/statistics_graph.php
mod/quiz/report/statistics/statistics_question_table.php [new file with mode: 0644]
mod/quiz/report/statistics/statistics_table.php
mod/quiz/report/statistics/version.php
question/type/calculated/questiontype.php
question/type/match/questiontype.php
question/type/multianswer/questiontype.php
question/type/numerical/questiontype.php
question/type/questiontype.php
question/type/randomsamatch/questiontype.php
question/type/shortanswer/questiontype.php

index 2642cec492f760720c1e62a53d6c7a249e3697ce..1a6ff7d3569cd4810e3d2499dfcf999cd04bc8f7 100644 (file)
@@ -49,8 +49,7 @@ $string['erroritemappearsmorethanoncewithdifferentweight'] = 'Question ($a) appe
 $string['lastcalculated'] = 'Last calculated $a->lastcalculated ago there have been $a->count attempts since then.';
 $string['recalculatenow'] = 'Recalculate now';
 $string['detailedanalysis'] = 'More detailed analysis of the responses to this question';
-$string['errordeletingquizstats'] = 'Error deleting old quiz_statistics records.';
-$string['errordeletingqstats'] = 'Error deleting old quiz_question_statistics records.';
+$string['errordeleting'] = 'Error deleting old $a records.';
 $string['questionname'] = 'Question Name';
 $string['questiontype'] = 'Question Type';
 $string['positions'] = 'Position(s)';
@@ -59,4 +58,12 @@ $string['questioninformation'] = 'Question information';
 $string['questionstatistics'] = 'Question statistics';
 $string['analysisofresponses'] = 'Analysis of responses';
 $string['statisticsreportgraph'] = 'Statistics for question positions';
+$string['response'] = 'Answer';
+$string['optiongrade'] = 'Partial credit';
+$string['count'] = 'Count';
+$string['frequency'] = 'Frequency';
+$string['backtoquizreport'] = 'Back to main statistics report page.';
+$string['analysisofresponsesfor'] = 'Analysis of responses for $a.';
+$string['downloadeverything'] = 'Download full report as';
+
 ?>
\ No newline at end of file
index bdca620a869f28de7684930c5cbae612234aa18d..9871f49f6e164d2517d2ce4472b2512167325787 100644 (file)
@@ -117,8 +117,12 @@ class flexible_table {
         return $this->download;
     }
     
-    function export_class_instance(){
-        if (is_null($this->exportclass) && !empty($this->download)){
+    function export_class_instance(&$exportclass=null){
+        if (!is_null($exportclass)){
+            $this->started_output = true;
+            $this->exportclass =& $exportclass;
+            $this->exportclass->table =& $this;
+        } elseif (is_null($this->exportclass) && !empty($this->download)){
             $classname = 'table_'.$this->download.'_export_format';
             $this->exportclass = new $classname($this);
             if (!$this->exportclass->document_started()){
@@ -752,7 +756,7 @@ class flexible_table {
      * This function is not part of the public api.
      */
     function print_initials_bar(){
-        if (($this->sess->i_last || $this->sess->i_first || $this->use_initials) 
+        if ((!empty($this->sess->i_last) || !empty($this->sess->i_first) || $this->use_initials) 
                     && isset($this->columns['fullname'])) {
 
             $strall = get_string('all');
@@ -1417,6 +1421,7 @@ class table_xhtml_export_format extends table_default_export_format_parent{
 
 <html xmlns="http://www.w3.org/1999/xhtml"
   xml:lang="en" lang="en">
+<head>
 <style type="text/css">/*<![CDATA[*/
 
 .flexible th {
@@ -1460,10 +1465,14 @@ h1, h2{
 .bold {
 font-weight:bold;
 }
-
+.mdl-align {
+    text-align:center;
+}
 
 
 /*]]>*/</style>
+<title>$filename</title>
+</head>
 <body>
 EOF;
         $this->documentstarted = true;
@@ -1491,7 +1500,7 @@ EOF;
         $this->table->finish_html();
     }
     function finish_document(){
-        echo '</body>';
+        echo "</body>\n</html>";
         exit;
     }
 }
index d81282e489210aa172d3046035d3f5dd10861c63..4d99e07b6a6de3bcaa60369b2918a24e9bf1c453 100644 (file)
@@ -46,14 +46,27 @@ function quiz_get_newgraded_states($attemptidssql, $idxattemptq = true, $fields=
         return $gradedstates;
     }
 }
-function quiz_report_index_by_keys($datum, $keys){
+/**
+ * Takes an array of objects and constructs a multidimensional array keyed by
+ * the keys it finds on the object.
+ * @param array $datum an array of objects with properties on the object
+ * including the keys passed as the next param.
+ * @param array $keys Array of strings with the names of the properties on the
+ * objects in datum that you want to index the multidimensional array by.
+ * @param boolean $keysunique If there is not only one object for each
+ * combination of keys you are using you should set $keysunique to true.
+ * Otherwise all the object will be added to a zero based array. So the array
+ * returned will have count($keys) + 1 indexs.
+ * @return array multidimensional array properly indexed.
+ */
+function quiz_report_index_by_keys($datum, $keys, $keysunique=true){
     if (!$datum){
         return $datum;
     }
     $key = array_shift($keys);
     $datumkeyed = array();
     foreach ($datum as $data){
-        if ($keys){
+        if ($keys || !$keysunique){
             $datumkeyed[$data->{$key}][]= $data;
         } else {
             $datumkeyed[$data->{$key}]= $data;
@@ -61,12 +74,25 @@ function quiz_report_index_by_keys($datum, $keys){
     }
     if ($keys){
         foreach ($datumkeyed as $datakey => $datakeyed){
-            $datumkeyed[$datakey] = quiz_report_index_by_keys($datakeyed, $keys);
+            $datumkeyed[$datakey] = quiz_report_index_by_keys($datakeyed, $keys, $keysunique);
         }
     }
     return $datumkeyed;
 }
-
+function quiz_report_unindex($datum){
+    if (!$datum){
+        return $datum;
+    }
+    $datumunkeyed = array();
+    foreach ($datum as $value){
+        if (is_array($value)){
+            $datumunkeyed = array_merge($datumunkeyed, quiz_report_unindex($value));
+        } else {
+            $datumunkeyed[] = $value;
+        }
+    }
+    return $datumunkeyed;
+}
 function quiz_get_regraded_qs($attemptidssql, $limitfrom=0, $limitnum=0){
     global $CFG, $DB;
     if ($attemptidssql && is_array($attemptidssql)){
@@ -306,13 +332,13 @@ function quiz_report_feedback_for_grade($grade, $quizid) {
 }
 
 function quiz_report_scale_sumgrades_as_percentage($rawgrade, $quiz, $round = true) {
-    if ($quiz->sumgrades) {
+    if ($quiz->sumgrades != 0) {
         $grade = $rawgrade * 100 / $quiz->sumgrades;
         if ($round) {
             $grade = quiz_format_grade($quiz, $grade);
         }
     } else {
-        $grade = 0;
+        return '';
     }
     return $grade.'%';
 }
index 13cb5fd3174bdd0b4e918fceee2f6a7cc1a110b5..08d69ea025195bf82efd4d28f511d265169d3904 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="mod/quiz/report/statistics/db" VERSION="20080728" COMMENT="XMLDB file for Moodle mod/quiz/report/statistics"
+<XMLDB PATH="mod/quiz/report/statistics/db" VERSION="20080908" COMMENT="XMLDB file for Moodle mod/quiz/report/statistics"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../../../../lib/xmldb/xmldb.xsd"
 >
@@ -27,7 +27,7 @@
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
       </KEYS>
     </TABLE>
-    <TABLE NAME="quiz_question_statistics" COMMENT="Default comment for the table, please edit me" PREVIOUS="quiz_statistics">
+    <TABLE NAME="quiz_question_statistics" COMMENT="Default comment for the table, please edit me" PREVIOUS="quiz_statistics" NEXT="quiz_question_response_stats">
       <FIELDS>
         <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="true" ENUM="false" NEXT="quizstatisticsid"/>
         <FIELD NAME="quizstatisticsid" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="false" ENUM="false" PREVIOUS="id" NEXT="questionid"/>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
       </KEYS>
     </TABLE>
+    <TABLE NAME="quiz_question_response_stats" COMMENT="Quiz question responses." PREVIOUS="quiz_question_statistics">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="true" ENUM="false" NEXT="quizstatisticsid"/>
+        <FIELD NAME="quizstatisticsid" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="false" ENUM="false" PREVIOUS="id" NEXT="questionid"/>
+        <FIELD NAME="questionid" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="false" ENUM="false" PREVIOUS="quizstatisticsid" NEXT="subqid"/>
+        <FIELD NAME="subqid" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="false" ENUM="false" PREVIOUS="questionid" NEXT="aid"/>
+        <FIELD NAME="aid" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="false" ENUM="false" PREVIOUS="subqid" NEXT="response"/>
+        <FIELD NAME="response" TYPE="text" LENGTH="big" NOTNULL="false" SEQUENCE="false" ENUM="false" PREVIOUS="aid" NEXT="rcount"/>
+        <FIELD NAME="rcount" TYPE="int" LENGTH="10" NOTNULL="false" UNSIGNED="true" SEQUENCE="false" ENUM="false" PREVIOUS="response" NEXT="credit"/>
+        <FIELD NAME="credit" TYPE="number" LENGTH="15" NOTNULL="true" UNSIGNED="true" SEQUENCE="false" ENUM="false" DECIMALS="5" PREVIOUS="rcount"/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+      </KEYS>
+    </TABLE>
   </TABLES>
 </XMLDB>
\ No newline at end of file
index d6ec6023a8d829b3b1ad6a455df03f449fb23f8a..4b65ff2c74f970a8e28dbf9da05a58a65d387092 100644 (file)
@@ -61,6 +61,62 @@ function xmldb_quizreport_statistics_upgrade($oldversion) {
         $dbman->change_field_type($table, $field);
     }
 
+    if ($result && $oldversion < 2008082600) {
+
+    /// Define table quiz_question_response_stats to be created
+        $table = new xmldb_table('quiz_question_response_stats');
+
+    /// Adding fields to table quiz_question_response_stats
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null, null, null);
+        $table->add_field('quizstatisticsid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, null, null);
+        $table->add_field('questionid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, null, null);
+        $table->add_field('anssubqid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, null, null, null, null, null);
+        $table->add_field('response', XMLDB_TYPE_TEXT, 'big', null, null, null, null, null, null);
+        $table->add_field('rcount', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, null, null, null, null, null);
+        $table->add_field('credit', XMLDB_TYPE_NUMBER, '15, 5', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, null, null);
+
+    /// Adding keys to table quiz_question_response_stats
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+
+    /// Conditionally launch create table for quiz_question_response_stats
+        if (!$dbman->table_exists($table)) {
+            $dbman->create_table($table);
+        }
+
+
+    }
+    if ($result && $oldversion < 2008090500) {
+        //delete all cached results first
+        $result = $result && $DB->delete_records('quiz_statistics');
+        $result = $result && $DB->delete_records('quiz_question_statistics');
+        $result = $result && $DB->delete_records('quiz_question_response_stats');
+        if ($result){
+        /// Define field anssubqid to be dropped from quiz_question_response_stats
+            $table = new xmldb_table('quiz_question_response_stats');
+            $field = new xmldb_field('anssubqid');
+    
+        /// Conditionally launch drop field subqid
+            if ($dbman->field_exists($table, $field)) {
+                $dbman->drop_field($table, $field);
+            }
+    
+        /// Define field subqid to be added to quiz_question_response_stats
+            $field = new xmldb_field('subqid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, null, null, 'questionid');
+    
+        /// Conditionally launch add field subqid
+            if (!$dbman->field_exists($table, $field)) {
+                $dbman->add_field($table, $field);
+            }
+    
+        /// Define field aid to be added to quiz_question_response_stats
+            $field = new xmldb_field('aid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, null, null, 'subqid');
+    
+        /// Conditionally launch add field aid
+            if (!$dbman->field_exists($table, $field)) {
+                $dbman->add_field($table, $field);
+            }
+        }
+    }
     return $result;
 }
 
index aeca387e9b52e88345f906f5b5bddef26cbc9699..15dfe71e985d1461c4f040739c32ac18ec7a1394 100644 (file)
@@ -9,6 +9,7 @@ class qstats{
     var $questions;
     var $subquestions = array();
     var $randomselectors = array();
+    var $responses = array();
     
     function qstats($questions, $s, $sumgradesavg){
         $this->s = $s;
@@ -75,7 +76,6 @@ class qstats{
     }
 
     function _secondary_states_walker($state, &$stats){
-        global $QTYPES;
         $gradedifference = ($state->grade - $stats->gradeaverage);
         $othergradedifference = (($state->sumgrades - $state->grade) - $stats->othergradeaverage);
         $overallgradedifference = $state->sumgrades - $this->sumgradesavg;
@@ -87,9 +87,49 @@ class qstats{
         $stats->covariancemaxsum += $sortedgradedifference * $sortedothergradedifference;
         $stats->covariancewithoverallgradesum += $gradedifference * $overallgradedifference;
 
+        if ($stats->subquestion){
+            $question =& $this->subquestions[$stats->questionid];
+        } else {
+            $question =& $this->questions[$stats->questionid];
+        }
+        $this->_process_actual_responses($question, $state);
+
+    }
 
+    function add_response_detail_to_array($responsedetail){
+        $responsedetail->rcount = 1;
+        if (isset($this->responses[$responsedetail->subqid])){
+            if (isset($this->responses[$responsedetail->subqid][$responsedetail->aid])){
+                if (isset($this->responses[$responsedetail->subqid][$responsedetail->aid][$responsedetail->response])){
+                    $this->responses[$responsedetail->subqid][$responsedetail->aid][$responsedetail->response]->rcount++;
+                } else {
+                    $this->responses[$responsedetail->subqid][$responsedetail->aid][$responsedetail->response] = $responsedetail;
+                }
+            } else {
+                $this->responses[$responsedetail->subqid][$responsedetail->aid] = array($responsedetail->response => $responsedetail);
+            }
+        } else {
+            $this->responses[$responsedetail->subqid] = array();
+            $this->responses[$responsedetail->subqid][$responsedetail->aid] = array($responsedetail->response => $responsedetail);
+        }
     }
 
+    /** 
+     * Get the data for the individual question response analysis table.
+     */
+    function _process_actual_responses($question, $state){
+        global $QTYPES;
+        if ($question->qtype != 'random' && 
+                $QTYPES[$question->qtype]->show_analysis_of_responses()){
+            $restoredstate = clone($state);
+            restore_question_state($question, $restoredstate);
+            $responsedetails = $QTYPES[$question->qtype]->get_actual_response_details($question, $restoredstate);
+            foreach ($responsedetails as $responsedetail){
+                $responsedetail->questionid = $question->id;
+                $this->add_response_detail_to_array($responsedetail);
+            }
+        }
+    }
 
     function _initial_question_walker(&$stats){
         $stats->gradeaverage = $stats->totalgrades / $stats->s;
@@ -129,6 +169,8 @@ class qstats{
     }
     
     function process_states(){
+        global $DB;
+        set_time_limit(0);
         $subquestionstats = array();
         foreach ($this->states as $state){
             $this->_initial_states_walker($state, $this->questions[$state->question]->_stats);
@@ -162,6 +204,7 @@ class qstats{
         foreach (array_keys($this->subquestions) as $qid){
             $this->subquestions[$qid]->_stats = $subquestionstats[$qid];
             $this->subquestions[$qid]->_stats->questionid = $qid;
+            $this->subquestions[$qid]->maxgrade = $this->subquestions[$qid]->_stats->maxgrade;
             $this->_initial_question_walker($this->subquestions[$qid]->_stats);
             if ($subquestionstats[$qid]->differentweights){
                 notify(get_string('erroritemappearsmorethanoncewithdifferentweight', 'quiz_statistics', $this->subquestions[$qid]->name));
@@ -220,6 +263,7 @@ class qstats{
                 $this->questions[$qid]->_stats->effectiveweight = null;
             }
         }
+        $this->responses = quiz_report_unindex($this->responses);
     }
     /**
      * Needed by quiz stats calculations.
index 294c095698a7f2325d21bde5cca59be2ed7b8c80..610c1e3e21f5c48890d16610ab37d9146549fa46 100644 (file)
@@ -23,11 +23,12 @@ class quiz_statistics_report extends quiz_default_report {
      * Display the report.
      */
     function display($quiz, $cm, $course) {
-        global $CFG, $DB;
+        global $CFG, $DB, $QTYPES;
 
         $context = get_context_instance(CONTEXT_MODULE, $cm->id);
 
         $download = optional_param('download', '', PARAM_ALPHA);
+        $everything = optional_param('everything', 0, PARAM_BOOL);
         $recalculate = optional_param('recalculate', 0, PARAM_BOOL);
         //pass the question id for detailed analysis question
         $qid = optional_param('qid', 0, PARAM_INT);
@@ -76,10 +77,13 @@ class quiz_statistics_report extends quiz_default_report {
             if ($todelete = $DB->get_records_menu('quiz_statistics', array('quizid' => $quiz->id, 'groupid'=> (int)$currentgroup, 'allattempts'=>$useallattempts))){
                 list($todeletesql, $todeleteparams) = $DB->get_in_or_equal(array_keys($todelete));
                 if (!$DB->delete_records_select('quiz_statistics', "id $todeletesql", $todeleteparams)){
-                    print_error('errordeletingquizstats', 'quiz_statistics');
+                    print_error('errordeleting', 'quiz_statistics', '', 'quiz_statistics');
                 }
                 if (!$DB->delete_records_select('quiz_question_statistics', "quizstatisticsid $todeletesql", $todeleteparams)){
-                    print_error('errordeletingqstats', 'quiz_statistics');
+                    print_error('errordeleting', 'quiz_statistics', '', 'quiz_question_statistics');
+                }
+                if (!$DB->delete_records_select('quiz_question_response_stats', "quizstatisticsid $todeletesql", $todeleteparams)){
+                    print_error('errordeleting', 'quiz_statistics', '', 'quiz_question_response_stats');
                 }
             }
             redirect($reporturl->out());
@@ -124,11 +128,35 @@ class quiz_statistics_report extends quiz_default_report {
         }
         
         if (!$qid){//main page
-            $this->output_quiz_stats_table($course, $cm, $quiz, $quizstats, $usingattemptsstring, $currentgroup, $groupstudents, $useallattempts, $download, $reporturl);
-            $this->output_question_stats_table($s, $questions, $subquestions);
-            $imageurl = $CFG->wwwroot.'/mod/quiz/report/statistics/statistics_graph.php?id='.$quizstats->id;
-            print_heading(get_string('statisticsreportgraph', 'quiz_statistics'));
-            echo '<div class="mdl-align"><img src="'.$imageurl.'" alt="'.get_string('statisticsreportgraph', 'quiz_statistics').'" /></div>';
+            $this->output_quiz_info_table($course, $cm, $quiz, $quizstats, $usingattemptsstring, $currentgroup, $groupstudents, $useallattempts, $download, $reporturl, $everything);
+            $this->output_quiz_structure_analysis_table($s, $questions, $subquestions);
+            if (!$this->table->is_downloading() || ($everything && $this->table->is_downloading() == 'xhtml')){
+                if ($s > 1){
+                    $imageurl = $CFG->wwwroot.'/mod/quiz/report/statistics/statistics_graph.php?id='.$quizstats->id;
+                    print_heading(get_string('statisticsreportgraph', 'quiz_statistics'));
+                    echo '<div class="mdl-align"><img src="'.$imageurl.'" alt="'.get_string('statisticsreportgraph', 'quiz_statistics').'" /></div>';
+                }
+            }
+            if ($this->table->is_downloading()){
+                if ($everything){
+                    foreach ($questions as $question){
+                        if ($question->qtype != 'random' && $QTYPES[$question->qtype]->show_analysis_of_responses()){
+                            $this->output_individual_question_data($quiz, $question, $reporturl, $quizstats);
+                        } elseif (!empty($question->_stats->subquestions)) {
+                            $subitemstodisplay = explode(',', $question->_stats->subquestions);
+                            foreach ($subitemstodisplay as $subitemid){
+                                $this->output_individual_question_data($quiz, $subquestions[$subitemid], $reporturl, $quizstats);
+                            }
+                        }
+                    }
+                    $exportclassinstance =& $this->table->export_class_instance();
+                } else {
+                    $this->table->finish_output();
+                }
+            }
+            if ($this->table->is_downloading() && $everything){
+                $exportclassinstance->finish_document();
+            }
         } else {//individual question page
             $thisquestion = false;
             if (isset($questions[$qid])){
@@ -138,64 +166,168 @@ class quiz_statistics_report extends quiz_default_report {
             } else {
                 print_error('questiondoesnotexist', 'question');
             }
-            $this->output_question_info_table($quiz, $thisquestion);
+            $this->output_individual_question_data($quiz, $thisquestion, $reporturl, $quizstats);
         }
         return true;
     }
     
-    function output_question_info_table($quiz, $question){
-        $datumfromtable = $this->table->format_row($question);
-
-        $questioninfotable = new object();
-        $questioninfotable->align = array('center', 'center');
-        $questioninfotable->width = '60%';
-        $questioninfotable->class = 'generaltable titlesleft';
-        
-        $questioninfotable->data = array();
-        $questioninfotable->data[] = array(get_string('modulename', 'quiz'), $quiz->name);
-        $questioninfotable->data[] = array(get_string('questionname', 'quiz_statistics'), $question->name.'&nbsp;'.$datumfromtable['actions']);
-        $questioninfotable->data[] = array(get_string('questiontype', 'quiz_statistics'), $question->qtype.'&nbsp;'.$datumfromtable['icon']);
-        $questioninfotable->data[] = array(get_string('positions', 'quiz_statistics'), $question->_stats->positions);
-
-        $questionstatstable = new object();
-        $questionstatstable->align = array('center', 'center');
-        $questionstatstable->width = '60%';
-        $questionstatstable->class = 'generaltable titlesleft';
-
-        unset($datumfromtable['number']);
-        unset($datumfromtable['icon']);
-        $actions = $datumfromtable['actions'];
-        unset($datumfromtable['actions']);
-        unset($datumfromtable['name']);
-        $labels = array('s' => get_string('attempts', 'quiz_statistics'),
-                        'facility' => get_string('facility', 'quiz_statistics'),
-                        'sd' => get_string('standarddeviationq', 'quiz_statistics'),
-                        'random_guess_score' => get_string('random_guess_score', 'quiz_statistics'),
-                        'intended_weight'=> get_string('intended_weight', 'quiz_statistics'),
-                        'effective_weight'=> get_string('effective_weight', 'quiz_statistics'),
-                        'discrimination_index'=> get_string('discrimination_index', 'quiz_statistics'),
-                        'discriminative_efficiency'=> get_string('discriminative_efficiency', 'quiz_statistics'));
-        foreach ($datumfromtable as $item => $value){
-            $questionstatstable->data[] = array($labels[$item], $value);
+    function sort_response_details($detail1, $detail2){
+        if ($detail1->credit == $detail2->credit){
+            return strcmp($detail1->answer, $detail2->answer);
         }
-
-        print_heading(get_string('questioninformation', 'quiz_statistics'));
-        print_table($questioninfotable);
-        
-        print_box(format_text($question->questiontext).$actions, 'boxaligncenter generalbox boxwidthnormal mdl-align');
-
-        print_heading(get_string('questionstatistics', 'quiz_statistics'));
-        print_table($questionstatstable);
-        
-        print_heading(get_string('analysisofresponses', 'quiz_statistics'));
-
+        return ($detail1->credit > $detail2->credit) ? -1 : 1;
     }
-        
-    function output_question_stats_table($s, $questions, $subquestions){
+    function sort_answers($answer1, $answer2){
+        if ($answer1->rcount == $answer2->rcount){
+            return strcmp($answer1->response, $answer2->response);
+        } else {
+            return ($answer1->rcount > $answer2->rcount)? -1 : 1;
+        }
+    }
+    
+    function output_individual_question_data($quiz, $question, $reporturl, $quizstats){
+        global $CFG, $DB, $QTYPES;
+        require_once($CFG->dirroot.'/mod/quiz/report/statistics/statistics_question_table.php');
+        $this->qtable = new quiz_report_statistics_question_table($question->id);
+        $downloadtype = $this->table->is_downloading();
+        if (!$this->table->is_downloading()){
+            $datumfromtable = $this->table->format_row($question);
+    
+            $questioninfotable = new object();
+            $questioninfotable->align = array('center', 'center');
+            $questioninfotable->width = '60%';
+            $questioninfotable->class = 'generaltable titlesleft';
+            
+            $questioninfotable->data = array();
+            $questioninfotable->data[] = array(get_string('modulename', 'quiz'), $quiz->name);
+            $questioninfotable->data[] = array(get_string('questionname', 'quiz_statistics'), $question->name.'&nbsp;'.$datumfromtable['actions']);
+            $questioninfotable->data[] = array(get_string('questiontype', 'quiz_statistics'), $datumfromtable['icon'].'&nbsp;'.get_string($question->qtype,'quiz').'&nbsp;'.$datumfromtable['icon']);
+            $questioninfotable->data[] = array(get_string('positions', 'quiz_statistics'), $question->_stats->positions);
+    
+            $questionstatstable = new object();
+            $questionstatstable->align = array('center', 'center');
+            $questionstatstable->width = '60%';
+            $questionstatstable->class = 'generaltable titlesleft';
+    
+            unset($datumfromtable['number']);
+            unset($datumfromtable['icon']);
+            $actions = $datumfromtable['actions'];
+            unset($datumfromtable['actions']);
+            unset($datumfromtable['name']);
+            $labels = array('s' => get_string('attempts', 'quiz_statistics'),
+                            'facility' => get_string('facility', 'quiz_statistics'),
+                            'sd' => get_string('standarddeviationq', 'quiz_statistics'),
+                            'random_guess_score' => get_string('random_guess_score', 'quiz_statistics'),
+                            'intended_weight'=> get_string('intended_weight', 'quiz_statistics'),
+                            'effective_weight'=> get_string('effective_weight', 'quiz_statistics'),
+                            'discrimination_index'=> get_string('discrimination_index', 'quiz_statistics'),
+                            'discriminative_efficiency'=> get_string('discriminative_efficiency', 'quiz_statistics'));
+            foreach ($datumfromtable as $item => $value){
+                $questionstatstable->data[] = array($labels[$item], $value);
+            }
+            print_heading(get_string('questioninformation', 'quiz_statistics'));
+            print_table($questioninfotable);
+            
+            print_box(format_text($question->questiontext).$actions, 'boxaligncenter generalbox boxwidthnormal mdl-align');
+    
+            print_heading(get_string('questionstatistics', 'quiz_statistics'));
+            print_table($questionstatstable);
+            
+        } else {
+            $this->qtable->export_class_instance($this->table->export_class_instance());
+            $questiontabletitle = !empty($question->number)?'('.$question->number.') ':'';
+            $questiontabletitle .= "\"{$question->name}\"";
+            $questiontabletitle = "<em>$questiontabletitle</em>";
+            if ($downloadtype == 'xhtml'){
+                $questiontabletitle = get_string('analysisofresponsesfor', 'quiz_statistics', $questiontabletitle);
+            }
+            $exportclass =& $this->table->export_class_instance();
+            $exportclass->start_table($questiontabletitle);
+        }
+        if ($QTYPES[$question->qtype]->show_analysis_of_responses()){
+            if (!$this->table->is_downloading()){
+                print_heading(get_string('analysisofresponses', 'quiz_statistics'));
+            }
+            $teacherresponses = $QTYPES[$question->qtype]->get_possible_responses($question);
+            $this->qtable->setup($reporturl, $question, count($teacherresponses)>1);
+            if ($this->table->is_downloading()){
+                $exportclass->output_headers($this->qtable->headers);
+            }
+    
+            $responses = $DB->get_records('quiz_question_response_stats', array('quizstatisticsid' => $quizstats->id, 'questionid' => $question->id), 'credit DESC, subqid ASC, aid ASC, rcount DESC');
+            $responses = quiz_report_index_by_keys($responses, array('subqid', 'aid'), false);
+            foreach ($responses as $subqid => $response){
+                foreach (array_keys($responses[$subqid]) as $aid){
+                    uasort($responses[$subqid][$aid], array('quiz_statistics_report', 'sort_answers'));
+                }
+                if (isset($responses[$subqid]['0'])){
+                    $wildcardresponse = new object();
+                    $wildcardresponse->answer = '*';
+                    $wildcardresponse->credit = 0;
+                    $teacherresponses[$subqid][0] = $wildcardresponse;
+                }
+            }
+            $first = true;
+            $subq = 0;
+            foreach ($teacherresponses as $subqid => $tresponsesforsubq){
+                $subq++;
+                $qhaswildcards = $QTYPES[$question->qtype]->has_wildcards_in_responses($question, $subqid);
+                if (!$first){
+                    $this->qtable->add_separator();
+                }
+                uasort($tresponsesforsubq, array('quiz_statistics_report', 'sort_response_details'));
+                foreach ($tresponsesforsubq as $aid => $teacherresponse){
+                    $teacherresponserow = new object();
+                    $teacherresponserow->response = $teacherresponse->answer; 
+                    $teacherresponserow->rcount = 0;
+                    $teacherresponserow->subq = $subq;
+                    $teacherresponserow->credit = $teacherresponse->credit;
+                    if (isset($responses[$subqid][$aid])){
+                        $singleanswer = count($responses[$subqid][$aid])==1 && 
+                                        ($responses[$subqid][$aid][0]->response == $teacherresponserow->response);
+                        if (!$singleanswer && $qhaswildcards){
+                            $this->qtable->add_separator();
+                        }
+                        foreach ($responses[$subqid][$aid] as $response){
+                            $teacherresponserow->rcount += $response->rcount;
+                        }
+                        if ($aid!=0 || $qhaswildcards){
+                            $this->qtable->add_data_keyed($this->qtable->format_row($teacherresponserow));
+                        }
+                        if (!$singleanswer){
+                            foreach ($responses[$subqid][$aid] as $response){
+                                if (!$downloadtype || $downloadtype=='xhtml'){
+                                    $indent = '&nbsp;&nbsp;&nbsp;&nbsp;';
+                                } else {
+                                    $indent = '    ';
+                                }
+                                $response->response = ($qhaswildcards?$indent:'').$response->response;
+                                $response->subq = $subq;
+                                if ((count($responses[$subqid][$aid])<2) || ($response->rcount > ($teacherresponserow->rcount / 10))){
+                                    $this->qtable->add_data_keyed($this->qtable->format_row($response));
+                                }
+                            }
+                        }
+                    } else {
+                        $this->qtable->add_data_keyed($this->qtable->format_row($teacherresponserow));
+                    }
+                }
+                $first = false;
+            }
+            $this->qtable->finish_output(!$this->table->is_downloading());
+        }
         if (!$this->table->is_downloading()){
-            print_heading(get_string('quizstructureanalysis', 'quiz_statistics'));
+            $url = $reporturl->out();
+            $text = get_string('backtoquizreport', 'quiz_statistics');
+            print_box("<a href=\"$url\">$text</a>", 'boxaligncenter generalbox boxwidthnormal mdl-align');
         }
+    }
+        
+    function output_quiz_structure_analysis_table($s, $questions, $subquestions){
         if ($s){
+            if (!$this->table->is_downloading()){
+                print_heading(get_string('quizstructureanalysis', 'quiz_statistics'));
+            }
             foreach ($questions as $question){
                 $this->table->add_data_keyed($this->table->format_row($question));
                 if (!empty($question->_stats->subquestions)){
@@ -207,11 +339,12 @@ class quiz_statistics_report extends quiz_default_report {
                 }
             }
 
-            $this->table->finish_output();
+            $this->table->finish_output(!$this->table->is_downloading());
         }
     }
     
-    function output_quiz_stats_table($course, $cm, $quiz, $quizstats, $usingattemptsstring, $currentgroup, $groupstudents, $useallattempts, $download, $reporturl){
+    function output_quiz_info_table($course, $cm, $quiz, $quizstats, $usingattemptsstring,
+                    $currentgroup, $groupstudents, $useallattempts, $download, $reporturl, $everything){
         global $DB;
         // Print information on the number of existing attempts
         $quizinformationtablehtml = print_heading(get_string('quizinformation', 'quiz_statistics'), '', 2, 'main', true);
@@ -280,11 +413,19 @@ class quiz_statistics_report extends quiz_default_report {
                                     get_string('recalculatenow', 'quiz_statistics'), 'post', '', true);
                 $quizinformationtablehtml .= print_box_end(true);
             }
+            $downloadoptions = $this->table->get_download_menu();
+            $quizinformationtablehtml .= '<form action="'. $this->table->baseurl .'" method="post">';
+            $quizinformationtablehtml .= '<div class="mdl-align">';
+            $quizinformationtablehtml .= '<input type="hidden" name="everything" value="1"/>';
+            $quizinformationtablehtml .= '<input type="submit" value="'.get_string('downloadeverything', 'quiz_statistics').'"/>';
+            $quizinformationtablehtml .= choose_from_menu ($downloadoptions, 'download', $this->table->defaultdownloadformat, '', '', '', true);
+            $quizinformationtablehtml .= helpbutton('tableexportformats', get_string('tableexportformats', 'table'), 'moodle', true, false, '', true);
+            $quizinformationtablehtml .= '</div></form>';
         }
         $quizinformationtablehtml .= print_table($quizinformationtable, true);
         if (!$this->table->is_downloading()){
             echo $quizinformationtablehtml;
-        } else {
+        } elseif ($everything) {
             $exportclass =& $this->table->export_class_instance();
             if ($download == 'xhtml'){
                 echo $quizinformationtablehtml;
@@ -315,8 +456,10 @@ class quiz_statistics_report extends quiz_default_report {
                     'FROM '.$fromqa.
                     'WHERE ' .$whereqa.
                     'GROUP BY (attempt=1)';
+            
             if (!$attempttotals = $DB->get_records_sql($sql, $qaparams)){
                 $s = 0;
+                $usingattemptsstring = '';
             } else {
                 $firstattempt = $attempttotals[1];
                 $allattempts = new object();
@@ -371,13 +514,7 @@ class quiz_statistics_report extends quiz_default_report {
             if (!$mediangrades = $DB->get_records_sql_menu($sql, $qaparams, $limitoffset, $limit)){
                 print_error('errormedian', 'quiz_statistics');
             }
-            if (count($mediangrades)==1){
-                $quizstats->median = array_shift($mediangrades);
-            } else {
-                $median = array_shift($mediangrades);
-                $median += array_shift($mediangrades);
-                $quizstats->median = $median /2;
-            }
+            $quizstats->median = array_sum($mediangrades) / count($mediangrades);
             if ($s>1){
                 //fetch sum of squared, cubed and power 4d 
                 //differences between grades and mean grade
@@ -434,7 +571,7 @@ class quiz_statistics_report extends quiz_default_report {
         if ($s>1){
             $p = count($qstats->questions);//no of positions
             if ($p > 1){
-                if ($k2){
+                if (isset($k2)){
                     $quizstats->cic = (100 * $p / ($p -1)) * (1 - ($qstats->sum_of_grade_variance())/$k2);
                     $quizstats->errorratio = 100 * sqrt(1-($quizstats->cic/100));
                     $quizstats->standarderror = ($quizstats->errorratio * $quizstats->standarddeviation / 100);
@@ -450,18 +587,24 @@ class quiz_statistics_report extends quiz_default_report {
         $params = array('quizid'=>$quiz->id, 'groupid'=>(int)$currentgroup, 'allattempts'=>$useallattempts, 'timemodified'=>$timemodified);
         if (!$quizstats = $DB->get_record_select('quiz_statistics', 'quizid = :quizid  AND groupid = :groupid AND allattempts = :allattempts AND timemodified > :timemodified', $params, '*', true)){
             list($s, $usingattemptsstring, $quizstats, $qstats) = $this->quiz_stats($nostudentsingroup, $quiz->id, $currentgroup, $groupstudents, $questions, $useallattempts);
-            $toinsert = (object)((array)$quizstats + $params);
-            $toinsert->timemodified = time();
-            $quizstats->id = $DB->insert_record('quiz_statistics', $toinsert);
-            foreach ($qstats->questions as $question){
-                $question->_stats->quizstatisticsid = $quizstats->id;
-                $DB->insert_record('quiz_question_statistics', $question->_stats, false, true);
-            }
-            foreach ($qstats->subquestions as $subquestion){
-                $subquestion->_stats->quizstatisticsid = $quizstats->id;
-                $DB->insert_record('quiz_question_statistics', $subquestion->_stats, false, true);
+            if ($s){
+                $toinsert = (object)((array)$quizstats + $params);
+                $toinsert->timemodified = time();
+                $quizstats->id = $DB->insert_record('quiz_statistics', $toinsert);
+                foreach ($qstats->questions as $question){
+                    $question->_stats->quizstatisticsid = $quizstats->id;
+                    $DB->insert_record('quiz_question_statistics', $question->_stats, false, true);
+                }
+                foreach ($qstats->subquestions as $subquestion){
+                    $subquestion->_stats->quizstatisticsid = $quizstats->id;
+                    $DB->insert_record('quiz_question_statistics', $subquestion->_stats, false, true);
+                }
+                foreach ($qstats->responses as $response){
+                    $response->quizstatisticsid = $quizstats->id;
+                    $DB->insert_record('quiz_question_response_stats', $response, false);
+                }
             }
-            if (isset($qstats)){
+            if ($qstats){
                 $questions = $qstats->questions;
                 $subquestions = $qstats->subquestions;
             } else {
@@ -477,6 +620,7 @@ class quiz_statistics_report extends quiz_default_report {
                 $usingattemptsstring = get_string('firstattempts', 'quiz_statistics');
                 $s = $quizstats->firstattemptscount;
             }
+            $subquestions = array();
             $questionstats = $DB->get_records('quiz_question_statistics', array('quizstatisticsid'=>$quizstats->id), 'subquestion ASC');
             $questionstats = quiz_report_index_by_keys($questionstats, array('subquestion', 'questionid'));
             if (1 < count($questionstats)){
@@ -486,12 +630,13 @@ class quiz_statistics_report extends quiz_default_report {
                 foreach (array_keys($subquestions) as $subqid){
                     $subquestions[$subqid]->_stats = $subquestionstats[$subqid];
                 }
-            } else {
+            } elseif (count($questionstats)) {
                 $mainquestionstats = $questionstats[0];
-                $subquestions = array();
             }
-            foreach (array_keys($questions) as $qid){
-                $questions[$qid]->_stats = $mainquestionstats[$qid];
+            if (count($questionstats)) {
+                foreach (array_keys($questions) as $qid){
+                    $questions[$qid]->_stats = $mainquestionstats[$qid];
+                }
             }
         }
         return array($quizstats, $questions, $subquestions, $s, $usingattemptsstring);
@@ -512,10 +657,5 @@ function quiz_report_attempts_sql($quizid, $currentgroup, $groupstudents, $allat
     }
     return array($fromqa, $whereqa, $qaparams);
 }
-function quiz_report_safe_divider($dividend, $divisor){
-    if ($divisor == 0){
-        return null;
-    }
-    return $dividend / $divisor;
-}
+
 ?>
index 4d906b0e77f1ebab67576b22f842be7d6c757f2f..15589e57d256be21e7b590c03a5d71e561a2a3d3 100644 (file)
@@ -27,7 +27,7 @@ if ($groupmode = groups_get_activity_groupmode($cm)) {   // Groups are being use
     $groups = false;
 }
 if ($quizstatistics->groupid){
-    if (!in_array($quizstatistics->groupid, $groups)){
+    if (!in_array($quizstatistics->groupid, array_keys($groups))){
         print_error('groupnotamember', 'group');
     }
 }
@@ -47,8 +47,10 @@ $line->parameter['legend_border'] = 'black';
 $line->parameter['legend_offset'] = 4;
 
 
-$line->parameter['bar_size']    = 1.2; 
-$line->parameter['bar_spacing'] = 10; 
+$line->parameter['bar_size']    = 1; 
+
+$line->parameter['zero_axis']        = 'grayEE';
+
 
 $fieldstoplot = array('facility' => get_string('facility', 'quiz_statistics'), 'discriminativeefficiency' => get_string('discriminative_efficiency', 'quiz_statistics'));
 $fieldstoplotfactor = array('facility' => 100, 'discriminativeefficiency' => 1);
@@ -60,26 +62,40 @@ foreach (array_keys($fieldstoplot) as $fieldtoplot){
             array('colour' => graph_get_new_colour(), 'bar' => 'fill', 'shadow_offset' => 1, 'legend' => $fieldstoplot[$fieldtoplot]);
 }
 foreach ($questionstatistics as $questionstatistic){
-    $line->x_data[] = $questions[$questionstatistic->questionid]->number;
+    $line->x_data[$questions[$questionstatistic->questionid]->number] = $questions[$questionstatistic->questionid]->number;
     foreach (array_keys($fieldstoplot) as $fieldtoplot){
         $value = !is_null($questionstatistic->$fieldtoplot)?$questionstatistic->$fieldtoplot:0;
         $value = $value * $fieldstoplotfactor[$fieldtoplot];
         $line->y_data[$fieldtoplot][$questions[$questionstatistic->questionid]->number] = $value;
     }
 }
+foreach (array_keys($line->y_data) as $fieldtoplot){
+    ksort($line->y_data[$fieldtoplot]);
+    $line->y_data[$fieldtoplot] = array_values($line->y_data[$fieldtoplot]);
+}
 ksort($line->x_data);
+$line->x_data = array_values($line->x_data);
 $max = 0;
 $min = 0;
 foreach (array_keys($fieldstoplot) as $fieldtoplot){
     ksort($line->y_data[$fieldtoplot]);
     $line->y_data[$fieldtoplot] = array_values($line->y_data[$fieldtoplot]);
     $max = (max($line->y_data[$fieldtoplot])> $max)? max($line->y_data[$fieldtoplot]): $max;
-    $min = (min($line->y_data[$fieldtoplot])> $min)? min($line->y_data[$fieldtoplot]): $min;
+    $min = (min($line->y_data[$fieldtoplot])< $min)? min($line->y_data[$fieldtoplot]): $min;
 }
 $line->y_order = array_keys($fieldstoplot);
 
+$gridresolution = 10;
+
+$max = ceil($max / $gridresolution) * $gridresolution;
+$min = floor($min / $gridresolution) * $gridresolution;
+
+$gridlines = ceil(($max - $min) / $gridresolution);
+
+
+$line->parameter['y_axis_gridlines'] = $gridlines+1;
 
-$line->parameter['y_min_left'] = $min;  // start at 0
+$line->parameter['y_min_left'] = $min;
 $line->parameter['y_max_left'] = $max;
 $line->parameter['y_decimal_left'] = 0; 
 
diff --git a/mod/quiz/report/statistics/statistics_question_table.php b/mod/quiz/report/statistics/statistics_question_table.php
new file mode 100644 (file)
index 0000000..aeb3cc3
--- /dev/null
@@ -0,0 +1,83 @@
+<?php  // $Id$
+require_once($CFG->libdir.'/tablelib.php');
+
+class quiz_report_statistics_question_table extends flexible_table {
+    /**
+     * @var object this question with _stats object.
+     */
+    var $question;
+
+    function quiz_report_statistics_question_table($qid){
+        parent::flexible_table('mod-quiz-report-statistics-question-table'.$qid);
+    }
+
+    /**
+     * Setup the columns and headers and other properties of the table and then
+     * call flexible_table::setup() method.
+     */
+    function setup($reporturl, $question, $hassubqs){
+        $this->question = $question;
+        // Define table columns
+        $columns = array();
+        $headers = array();
+        
+        if ($hassubqs){
+            $columns[]= 'subq';
+            $headers[]= '';
+        }
+
+        $columns[]= 'response';
+        $headers[]= get_string('response', 'quiz_statistics');
+        
+
+        $columns[]= 'credit';
+        $headers[]= get_string('optiongrade', 'quiz_statistics');
+        
+        $columns[]= 'rcount';
+        $headers[]= get_string('count', 'quiz_statistics');
+
+        $columns[]= 'frequency';
+        $headers[]= get_string('frequency', 'quiz_statistics');
+
+        $this->define_columns($columns);
+        $this->define_headers($headers);
+        $this->sortable(false);
+
+        $this->column_class('credit', 'numcol');
+        $this->column_class('rcount', 'numcol');
+        $this->column_class('frequency', 'numcol');
+
+        // Set up the table
+        $this->define_baseurl($reporturl->out());
+
+        $this->collapsible(false);
+
+        $this->set_attribute('class', 'generaltable generalbox boxaligncenter');
+
+        parent::setup();
+    }
+    
+    function col_subq($response){
+        return $response->subq;
+    }
+    
+    function col_credit($response){
+        if (!is_null($response->credit)){
+            return ($response->credit*100).'%';
+        } else {
+            return '';
+        }
+    }
+    
+    function col_frequency($response){
+        if ($this->question->_stats->s){
+            return format_float((($response->rcount / $this->question->_stats->s)*100),2).'%';
+        } else {
+            return '';
+        }
+    }
+
+
+
+}
+?>
index e2a33acb0de1ea9121451b4d78f252fbdf2c0e4d..d1d39552cd681bedba760fe51bcc7e541d81bf62 100644 (file)
@@ -78,18 +78,7 @@ class quiz_report_statistics_table extends flexible_table {
 
         $this->collapsible(true);
 
-/*        $this->column_suppress('picture');
-        $this->column_suppress('fullname');
-        $this->column_suppress('idnumber');
 
-        $this->no_sorting('feedbacktext');
-
-        $this->column_class('picture', 'picture');
-        $this->column_class('lastname', 'bold');
-        $this->column_class('firstname', 'bold');
-        $this->column_class('fullname', 'bold');
-        $this->column_class('sumgrades', 'bold');*/
-        
 
         $this->set_attribute('id', 'questionstatistics');
         $this->set_attribute('class', 'generaltable generalbox boxaligncenter');
@@ -124,7 +113,7 @@ class quiz_report_statistics_table extends flexible_table {
         return quiz_question_action_icons($this->quiz, $this->cmid, $question, $this->baseurl);
     }
     function col_qtype($question){
-        return $question->qtype;
+        return get_string($question->qtype,'quiz');
     }
     function col_intended_weight($question){
         return quiz_report_scale_sumgrades_as_percentage($question->_stats->maxgrade, $this->quiz);
index 4e37fd381a958e1a73fc9eaa5d0acc5f5ea19706..e08b511b26d64089dfed1d8071d64007629c85f0 100644 (file)
@@ -1,4 +1,4 @@
 <?php
-$plugin->version  = 2008081500;   // The (date) version of this module
+$plugin->version  = 2008090500;   // The (date) version of this module
 
 ?>
\ No newline at end of file
index 11250055a502cc34b4eda15d8a2f91c703e1f1d6..b71606231a9536e8708b384196be300cec083e93 100644 (file)
@@ -18,6 +18,14 @@ class question_calculated_qtype extends question_dataset_dependent_questiontype
         return 'calculated';
     }
 
+    function has_wildcards_in_responses($question, $subqid) {
+        return true;
+    }
+
+    function requires_qtypes() {
+        return array('numerical');
+    }
+
     function get_question_options(&$question) {
         // First get the datasets and default options
          global $CFG, $DB;
index 7efdf32a1b6d2ac4dbed6da500f100be87c5baa0..b5d62c89aa9077328f3106a9af880bb0c214d45e 100644 (file)
@@ -148,6 +148,13 @@ class question_match_qtype extends default_questiontype {
 
     function restore_session_and_responses(&$question, &$state) {
         global $DB;
+        static $subquestions = array();
+        if (!isset($subquestions[$question->id])){
+            if (!$subquestions[$question->id] = $DB->get_records('question_match_sub', array('question' => $question->id), 'id ASC')) {
+               notify('Error: Missing subquestions!');
+               return false;
+            }
+        }
         // The serialized format for matching questions is a comma separated
         // list of question answer pairs (e.g. 1-1,2-3,3-2), where the ids of
         // both refer to the id in the table question_match_sub.
@@ -155,17 +162,14 @@ class question_match_qtype extends default_questiontype {
         $responses = array_map(create_function('$val',
          'return explode("-", $val);'), $responses);
 
-        if (!$questions = $DB->get_records('question_match_sub', array('question' => $question->id), 'id ASC')) {
-           notify('Error: Missing subquestions!');
-           return false;
-        }
+
 
         // Restore the previous responses and place the questions into the state options
         $state->responses = array();
         $state->options->subquestions = array();
         foreach ($responses as $response) {
             $state->responses[$response[0]] = $response[1];
-            $state->options->subquestions[$response[0]] = $questions[$response[0]];
+            $state->options->subquestions[$response[0]] = clone($subquestions[$question->id][$response[0]]);
         }
 
         foreach ($state->options->subquestions as $key => $subquestion) {
@@ -406,6 +410,21 @@ class question_match_qtype extends default_questiontype {
         return $result;
     }
 
+    function get_possible_responses(&$question) {
+        $answers = array();
+        if (is_array($question->options->subquestions)) {
+            foreach ($question->options->subquestions as $subqid => $answer) {
+                if ($answer->questiontext) {
+                    $r = new stdClass;
+                    $r->answer = $answer->questiontext . ": " . $answer->answertext;
+                    $r->credit = 1;
+                    $answers[$subqid] = array($answer->id =>$r);
+                }
+            }
+        }
+        return $answers;
+    }
+    
     // ULPGC ecastro
     function get_actual_response($question, $state) {
        $subquestions = &$state->options->subquestions;
@@ -420,7 +439,33 @@ class question_match_qtype extends default_questiontype {
        }
        return $results;
    }
-
+   
+   function get_actual_response_details($question, $state) {
+        $responses = $this->get_actual_response($question, $state);
+        $teacherresponses = $this->get_possible_responses($question, $state);
+        //only one response
+        $responsedetails =array();
+        foreach ($responses as $tsubqid => $response){
+            $responsedetail = new object();
+            $responsedetail->subqid = $tsubqid;
+            $responsedetail->response = $response;
+            foreach ($teacherresponses[$tsubqid] as $aid => $tresponse){
+                if ($tresponse->answer == $response){
+                    $responsedetail->aid = $aid;
+                    break;
+                }
+            }
+            if (isset($responsedetail->aid)){
+                $responsedetail->credit = $teacherresponses[$tsubqid][$aid]->credit;
+            } else {
+                $responsedetail->aid = 0;
+                $responsedetail->credit = 0;
+            }
+            $responsedetails[] = $responsedetail;
+        }
+        return $responsedetails;
+    }
+    
     function response_summary($question, $state, $length=80) {
         return shorten_text(implode(', ', $this->get_actual_response($question, $state)), $length);
     }
index 03f2ec17e7aad15d89a34ea18520c34b56f84655..adab307b071179b731a5c4834013c2c00f3ce814 100644 (file)
@@ -22,6 +22,21 @@ class embedded_cloze_qtype extends default_questiontype {
     function name() {
         return 'multianswer';
     }
+    
+    function has_wildcards_in_responses($question, $subqid) {
+        global $QTYPES;
+        foreach ($question->options->questions as $subq){
+            if ($subq->id == $subqid){
+                return $QTYPES[$subq->qtype]->has_wildcards_in_responses($subq, $subqid);
+            }
+        }
+        notify('Could not find sub question!');
+        return true;
+    }
+
+    function requires_qtypes() {
+        return array('shortanswer', 'numerical', 'multichoice');
+    }
 
     function get_question_options(&$question) {
         global $QTYPES, $DB;
@@ -233,6 +248,36 @@ class embedded_cloze_qtype extends default_questiontype {
         return $responses;
     }
 
+    function get_possible_responses(&$question) {
+        global $QTYPES;
+        $responses = array();
+        foreach($question->options->questions as $key => $wrapped) {
+            if ($wrapped != ''){
+                if ($correct = $QTYPES[$wrapped->qtype]->get_possible_responses($wrapped)) {
+                    $responses += $correct;
+                } else {
+                    // if there is no correct answer to this subquestion then there
+                    // can not be a correct answer to the whole question either, so
+                    // we have to return null.
+                    return null;
+                }
+            }
+        }
+        return $responses;
+    }
+    function get_actual_response_details($question, $state){
+        global $QTYPES;
+        $details = array();
+        foreach($question->options->questions as $key => $wrapped) {
+            if ($wrapped != ''){
+                $stateforquestion = clone($state);
+                $stateforquestion->responses[''] = $state->responses[$key];
+                $details = array_merge($details, $QTYPES[$wrapped->qtype]->get_actual_response_details($wrapped, $stateforquestion));
+            }
+        }
+        return $details;
+    }
+
     function print_question_formulation_and_controls(&$question, &$state, $cmoptions, $options) {
 
         global $QTYPES, $CFG, $USER;
index 706a3cf316bbe2b20e65ff9af2d7d5b4856e3c52..00d31d146ca2fad89cc8311fcae4d5b5a1f3ed14 100644 (file)
@@ -25,6 +25,14 @@ class question_numerical_qtype extends question_shortanswer_qtype {
     function name() {
         return 'numerical';
     }
+    
+    function has_wildcards_in_responses() {
+        return true;
+    }
+
+    function requires_qtypes() {
+        return array('shortanswer');
+    }
 
     function get_question_options(&$question) {
         // Get the question answers and their respective tolerances
index 72ef1a85d0073ef8aeb6864db178d3f8e1f10748..9610c112116af4340a6243817929cd1c070f7405 100644 (file)
@@ -44,12 +44,24 @@ class default_questiontype {
     }
 
     /**
-     * The name this question should appear as in the create new question
-     * dropdown.
+     * Returns a list of other question types that this one requires in order to
+     * work. For example, the calculated question type is a subclass of the
+     * numerical question type, which is a subclass of the shortanswer question
+     * type; and the randomsamatch question type requires the shortanswer type
+     * to be installed.
      *
-     * @return mixed the desired string, or false to hide this question type in the menu.
+     * @return array any other question types that this one relies on. An empty
+     * array if none.
      */
-    function menu_name() {
+    function requires_qtypes() {
+        return array();
+    }
+
+    /**
+     * @return string the name of this question type in the user's language.
+     * You should not need to override this method, the default behaviour should be fine.
+     */
+    function local_name() {
         $name = $this->name();
         $menu_name = get_string($name, 'qtype_' . $name);
         if ($menu_name[0] == '[') {
@@ -60,6 +72,18 @@ class default_questiontype {
         return $menu_name;
     }
 
+    /**
+     * The name this question should appear as in the create new question
+     * dropdown. Override this method to return false if you don't want your
+     * question type to be createable, for example if it is an abstract base type,
+     * otherwise, you should not need to override this method.
+     *
+     * @return mixed the desired string, or false to hide this question type in the menu.
+     */
+    function menu_name() {
+        return $this->local_name();
+    }
+
     /**
      * @return boolean true if this question can only be graded manually.
      */
@@ -67,6 +91,15 @@ class default_questiontype {
         return false;
     }
 
+    /**
+     * @return boolean true if a table analyzing responses should be shown in
+     * the quiz statistics report. Usually if a question is manually graded
+     * then this analysis table won't be a good idea.
+     */
+    function show_analysis_of_responses() {
+        return !$this->is_manual_graded();
+    }
+
     /**
      * @return boolean true if this question type can be used by the random question type.
      */
@@ -74,6 +107,18 @@ class default_questiontype {
         return true;
     }
 
+    /**
+     * @param question record.
+     * @param integer subqid this is the id of the subquestion. Usually the id
+     * of the question record of the question record but this is dependent on
+     * the question type. Not relevant to some question types.
+     * @return whether the teacher supplied responses can include wildcards. Can
+     * more than one answer be equivalent to one teacher supplied response.
+     */
+    function has_wildcards_in_responses($question, $subqid) {
+        return false;
+    }
+    
     /**
      * @return whether the question_answers.answer field needs to have
      * restore_decode_content_links_worker called on it.
@@ -666,6 +711,20 @@ class default_questiontype {
             return null;
         }
     }
+    /**
+     * The difference between this method an get_all_responses is that this
+     * method is not passed a state object. It is the possible answers to a
+     * question no matter what the state.
+     * This method is not called for random questions.
+     * @return array of possible answers.
+     */
+    function get_possible_responses(&$question) {
+        static $responses = array();
+        if (!isset($responses[$question->id])){
+            $responses[$question->id] = $this->get_all_responses($question, new object());
+        }
+        return array($question->id => $responses[$question->id]->responses);
+    }
     
     /**
      * @param object $question
@@ -699,6 +758,33 @@ class default_questiontype {
        return $responses;
     }
 
+    function get_actual_response_details($question, $state) {
+        $response = array_shift($this->get_actual_response($question, $state));
+        $teacherresponses = $this->get_possible_responses($question, $state);
+        //only one response
+        list($tsubqid, $tresponses) = each($teacherresponses);
+        $responsedetail = new object();
+        $responsedetail->subqid = $tsubqid;
+        $responsedetail->response = $response;
+        if ($aid = $this->check_response($question, $state)){
+            $responsedetail->aid = $aid;
+        } else {
+            foreach ($tresponses as $aid => $tresponse){
+                if ($tresponse->answer == $response){
+                    $responsedetail->aid = $aid;
+                    break;
+                }
+            }
+        }
+        if (isset($responsedetail->aid)){
+            $responsedetail->credit = $tresponses[$aid]->credit;
+        } else {
+            $responsedetail->aid = 0;
+            $responsedetail->credit = 0;
+        }
+        return array($responsedetail);
+    }
+
     // ULPGC ecastro
     function get_fractional_grade(&$question, &$state) {
         $maxgrade = $question->maxgrade;
index 0879a67c387bb51f3a88abaf18a675ee39c3b49d..56fb4d36abe7b337cedec301a7d197cd5a7a9f9e 100644 (file)
@@ -19,6 +19,10 @@ class question_randomsamatch_qtype extends question_match_qtype {
         return 'randomsamatch';
     }
 
+    function requires_qtypes() {
+        return array('shortanswer');
+    }
+
     function is_usable_by_random() {
         return false;
     }
@@ -174,6 +178,7 @@ class question_randomsamatch_qtype extends question_match_qtype {
     function restore_session_and_responses(&$question, &$state) {
         global $DB;
         global $QTYPES;
+        static $wrappedquestions = array();
         if (empty($state->responses[''])) {
             $question->questiontext = "Insufficient selection options are
              available for this question, therefore it is not available in  this
@@ -188,29 +193,33 @@ class question_randomsamatch_qtype extends question_match_qtype {
             // Restore the previous responses
             $state->responses = array();
             foreach ($responses as $response) {
-                $state->responses[$response[0]] = $response[1];
-                if (!$wrappedquestion = $DB->get_record('question', array('id' => $response[0]))) {
-                    notify("Couldn't get question (id=$response[0])!");
-                    return false;
-                }
-                if (!$QTYPES[$wrappedquestion->qtype]
-                 ->get_question_options($wrappedquestion)) {
-                    notify("Couldn't get question options (id=$response[0])!");
-                    return false;
-                }
-
-                // Now we overwrite the $question->options->answers field to only
-                // *one* (the first) correct answer. This loop can be deleted to
-                // take all answers into account (i.e. put them all into the
-                // drop-down menu.
-                $foundcorrect = false;
-                foreach ($wrappedquestion->options->answers as $answer) {
-                    if ($foundcorrect || $answer->fraction != 1.0) {
-                        unset($wrappedquestion->options->answers[$answer->id]);
-                    } else if (!$foundcorrect) {
-                        $foundcorrect = true;
+                $wqid = $response[0];
+                $state->responses[$wqid] = $response[1];
+                if (!isset($wrappedquestions[$wqid])){
+                    if (!$wrappedquestions[$wqid] = $DB->get_record('question', array('id' => $wqid))) {
+                        notify("Couldn't get question (id=$wqid)!");
+                        return false;
+                    }
+                    if (!$QTYPES[$wrappedquestions[$wqid]->qtype]
+                     ->get_question_options($wrappedquestions[$wqid])) {
+                        notify("Couldn't get question options (id=$response[0])!");
+                        return false;
+                    }
+    
+                    // Now we overwrite the $question->options->answers field to only
+                    // *one* (the first) correct answer. This loop can be deleted to
+                    // take all answers into account (i.e. put them all into the
+                    // drop-down menu.
+                    $foundcorrect = false;
+                    foreach ($wrappedquestions[$wqid]->options->answers as $answer) {
+                        if ($foundcorrect || $answer->fraction != 1.0) {
+                            unset($wrappedquestions[$wqid]->options->answers[$answer->id]);
+                        } else if (!$foundcorrect) {
+                            $foundcorrect = true;
+                        }
                     }
                 }
+                $wrappedquestion = clone($wrappedquestions[$wqid]);
 
                 if (!$QTYPES[$wrappedquestion->qtype]
                  ->restore_session_and_responses($wrappedquestion, $state)) {
@@ -272,6 +281,61 @@ class question_randomsamatch_qtype extends question_match_qtype {
         $result->responses = $answers;
         return $result;
     }
+    /**
+     * The difference between this method an get_all_responses is that this
+     * method is not passed a state object. It is the possible answers to a
+     * question no matter what the state.
+     * This method is not called for random questions.
+     * @return array of possible answers.
+     */
+    function get_possible_responses(&$question) {
+        global $QTYPES;
+        static $answers = array();
+        if (!isset($answers[$question->id])){
+            if ($question->options->subcats) {
+                // recurse into subcategories
+                $categorylist = question_categorylist($question->category);
+            } else {
+                $categorylist = $question->category;
+            }
+    
+            $question->options->subquestions = $this->get_sa_candidates($categorylist);
+            foreach ($question->options->subquestions as $key => $wrappedquestion) {
+                if (!$QTYPES[$wrappedquestion->qtype]
+                 ->get_question_options($wrappedquestion)) {
+                    return false;
+                }
+    
+                // Now we overwrite the $question->options->answers field to only
+                // *one* (the first) correct answer. This loop can be deleted to
+                // take all answers into account (i.e. put them all into the
+                // drop-down menu.
+                $foundcorrect = false;
+                foreach ($wrappedquestion->options->answers as $answer) {
+                    if ($foundcorrect || $answer->fraction != 1.0) {
+                        unset($wrappedquestion->options->answers[$answer->id]);
+                    } else if (!$foundcorrect) {
+                        $foundcorrect = true;
+                    }
+                }
+            }
+            $answers[$question->id] = array();
+            if (is_array($question->options->subquestions)) {
+                foreach ($question->options->subquestions as $subqid => $answer) {
+                    if ($answer->questiontext) {
+                        $ans = array_shift($answer->options->answers);
+                        $answer->answertext = $ans->answer ;
+                        $r = new stdClass;
+                        $r->answer = $answer->questiontext . ": " . $answer->answertext;
+                        $r->credit = 1;
+                        $answers[$question->id][$subqid] = array($ans->id => $r);
+                    }
+                }
+            }
+        }
+        return $answers[$question->id];
+    }
+
     /**
      * @param object $question
      * @return mixed either a integer score out of 1 that the average random
index a01d1da30c807bc6e5f524bb1568da24124a6b75..cdf6e149db3ddf92e8f86f13b4ae7b031c1f69d3 100644 (file)
@@ -22,6 +22,10 @@ class question_shortanswer_qtype extends default_questiontype {
         return 'shortanswer';
     }
 
+    function has_wildcards_in_responses($question, $subqid) {
+        return true;
+    }
+
     function get_question_options(&$question) {
         global $DB;
         // Get additional information from database