Commit 43ec99aa authored by jamiesensei's avatar jamiesensei
Browse files

MDL-15268 "Content for Quiz Statistics report table" further work on quiz statistics report.

parent 8ba751ee
......@@ -45,4 +45,16 @@ $string['discriminative_efficiency'] = 'Discriminative Efficiency';
$string['effective_weight'] = 'Effective weight';
$string['errorrandom'] = 'Error getting sub item data';
$string['erroritemappearsmorethanoncewithdifferentweight'] = 'Question ($a) appears more than once with different weights in different positions of the test. This is not currently supported by the statistics report and may make the statistics for this question unreliable.';
$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['questionname'] = 'Question Name';
$string['questiontype'] = 'Question Type';
$string['positions'] = 'Position(s)';
$string['position'] = 'Position';
$string['questioninformation'] = 'Question information';
$string['questionstatistics'] = 'Question statistics';
$string['analysisofresponses'] = 'Analysis of responses';
?>
\ No newline at end of file
......@@ -2483,18 +2483,23 @@ function get_filesdir_from_context($context){
return $courseid;
}
/**
* Get the real question id for a random question.
* Get the real state - the correct question id and answer - for a random
* question.
* @param object $state with property answer.
* @return mixed return integer real question id or false if there was an
* error..
*/
function question_get_real_questionid($state){
function question_get_real_state($state){
$realstate = clone($state);
$matches = array();
if (!preg_match('|^random([0-9]+)-|', $state->answer, $matches)){
if (!preg_match('|^random([0-9]+)-(.+)|', $state->answer, $matches)){
notify(get_string('errorrandom', 'quiz_statistics'));
return false;
} else {
return $matches[1];
$realstate->question = $matches[1];
$realstate->answer = $matches[2];
return $realstate;
}
}
?>
......@@ -108,17 +108,27 @@ class flexible_table {
*/
function is_downloading($download = null, $filename='', $sheettitle=''){
if ($download!==null){
$this->filename = clean_filename($filename);
$this->sheettitle = $sheettitle;
$this->is_downloadable(true);
$this->download = $download;
if (!empty($download)){
$classname = 'table_'.$download.'_export_format';
$this->exportclass = new $classname($this);
}
$this->filename = clean_filename($filename);
$this->export_class_instance();
}
return $this->download;
}
function export_class_instance(){
if (is_null($this->exportclass) && !empty($this->download)){
$classname = 'table_'.$this->download.'_export_format';
$this->exportclass = new $classname($this);
if (!$this->exportclass->document_started()){
$this->exportclass->start_document($this->filename);
}
}
return $this->exportclass;
}
/**
* Probably don't need to call this directly. Calling is_downloading with a
* param automatically sets table as downloadable.
......@@ -625,9 +635,12 @@ class flexible_table {
* data to the table with add_data or add_data_keyed.
*
*/
function finish_output(){
function finish_output($closeexportclassdoc = true){
if ($this->exportclass!==null){
$this->exportclass->finish_output();
$this->exportclass->finish_table();
if ($closeexportclassdoc){
$this->exportclass->finish_document();
}
}else{
$this->finish_html();
}
......@@ -853,7 +866,7 @@ class flexible_table {
function start_output(){
$this->started_output = true;
if ($this->exportclass!==null){
$this->exportclass->start_output($this->filename, $this->sheettitle);
$this->exportclass->start_table($this->sheettitle);
$this->exportclass->output_headers($this->headers);
} else {
$this->start_html();
......@@ -1234,9 +1247,19 @@ class table_default_export_format_parent{
* object from which to export data.
*/
var $table;
/**
* @var boolean output started. Keeps track of whether any output has been
* started yet.
*/
var $documentstarted = false;
function table_default_export_format_parent(&$table){
$this->table =& $table;
}
function set_table(&$table){
$this->table =& $table;
}
function add_data($row) {
return false;
......@@ -1244,7 +1267,8 @@ class table_default_export_format_parent{
function add_seperator() {
return false;
}
function finish_output(){
function document_started(){
return $this->documentstarted;
}
}
......@@ -1271,22 +1295,21 @@ class table_spreadsheet_export_format_parent extends table_default_export_format
*/
function define_workbook(){
}
function start_output($filename, $sheettitle){
$this->filename = $filename.'.'.$this->fileextension;
function start_document($filename){
$filename = $filename.'.'.$this->fileextension;
$this->define_workbook();
// Creating the first worksheet
$this->worksheet =& $this->workbook->add_worksheet();
// format types
$this->formatnormal =& $this->workbook->add_format();
$this->formatnormal->set_bold(0);
$this->formatheaders =& $this->workbook->add_format();
$this->formatheaders->set_bold(1);
$this->formatheaders->set_align('center');
// Sending HTTP headers
$this->workbook->send($this->filename);
// Creating the first worksheet
$this->workbook->send($filename);
$this->documentstarted = true;
}
function start_table($sheettitle){
$this->worksheet =& $this->workbook->add_worksheet($sheettitle);
$this->rownum=0;
}
function output_headers($headers){
......@@ -1310,7 +1333,10 @@ class table_spreadsheet_export_format_parent extends table_default_export_format
$this->rownum++;
return true;
}
function finish_output(){
function finish_table(){
}
function finish_document(){
$this->workbook->close();
exit;
}
......@@ -1340,23 +1366,29 @@ class table_ods_export_format extends table_spreadsheet_export_format_parent{
class table_text_export_format_parent extends table_default_export_format_parent{
var $seperator = "\t";
function start_output($filename, $sheettitle){
function start_document($filename){
$this->filename = $filename.".txt";
header("Content-Type: application/download\n");
header("Content-Disposition: attachment; filename=\"$this->filename\"");
header("Content-Disposition: attachment; filename=\"{$filename}.txt\"");
header("Expires: 0");
header("Cache-Control: must-revalidate,post-check=0,pre-check=0");
header("Pragma: public");
$this->documentstarted = true;
}
function start_table($sheettitle){
//nothing to do here
}
function output_headers($headers){
echo implode($this->seperator, $headers)." \n";
echo implode($this->seperator, $headers)."\n";
}
function add_data($row){
echo implode($this->seperator, $row)." \n";
echo implode($this->seperator, $row)."\n";
return true;
}
function finish_output(){
function finish_table(){
echo "\n\n";
}
function finish_document(){
exit;
}
}
......@@ -1372,20 +1404,13 @@ class table_csv_export_format extends table_text_export_format_parent{
}
class table_xhtml_export_format extends table_default_export_format_parent{
var $seperator = "\t";
function start_output($filename, $sheettitle){
$this->table->sortable(false);
$this->table->collapsible(false);
$this->filename = $filename.".html";
function start_document($filename){
header("Content-Type: application/download\n");
header("Content-Disposition: attachment; filename=\"$this->filename\"");
header("Content-Disposition: attachment; filename=\"$filename.html\"");
header("Expires: 0");
header("Cache-Control: must-revalidate,post-check=0,pre-check=0");
header("Pragma: public");
//html headers
echo <<<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html
......@@ -1431,7 +1456,7 @@ table {
margin:auto;
}
h1{
h1, h2{
text-align:center;
}
.bold {
......@@ -1441,14 +1466,18 @@ font-weight:bold;
/*]]>*/</style>
<head>
<title>$sheettitle</title>
</head>
<body>
<h1>$sheettitle</h1>
EOF;
$this->documentstarted = true;
}
function start_table($sheettitle){
$this->table->sortable(false);
$this->table->collapsible(false);
echo "<h2>{$sheettitle}</h2>";
$this->table->start_html();
}
function output_headers($headers){
$this->table->print_headers();
}
......@@ -1460,8 +1489,10 @@ EOF;
$this->table->print_row(NULL);
return true;
}
function finish_output(){
function finish_table(){
$this->table->finish_html();
}
function finish_document(){
echo '</body>';
exit;
}
......
......@@ -312,6 +312,6 @@ function quiz_report_scale_sumgrades_as_percentage($rawgrade, $quiz, $round = tr
} else {
$grade = 0;
}
return $grade.' %';
return $grade.'%';
}
?>
......@@ -2,7 +2,7 @@
function quiz_report_statistics_cron(){
global $DB;
if ($todelete = $DB->get_records_select_menu('quiz_statistics', 'timemodified < ?', array(time()-5*HOURSECS))){
list($todeletesql, $todeleteparams) = $DB->get_in_or_equal($todelete);
list($todeletesql, $todeleteparams) = $DB->get_in_or_equal(array_keys($todelete));
if (!$DB->delete_records_select('quiz_statistics', "id $todeletesql", $todeleteparams)){
mtrace('Error deleting out of date quiz_statistics records.');
}
......
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="mod/quiz/report/statistics/db" VERSION="20080725" COMMENT="XMLDB file for Moodle mod/quiz/report/statistics"
<XMLDB PATH="mod/quiz/report/statistics/db" VERSION="20080728" COMMENT="XMLDB file for Moodle mod/quiz/report/statistics"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../../../lib/xmldb/xmldb.xsd"
>
......@@ -39,7 +39,9 @@
<FIELD NAME="discriminativeefficiency" TYPE="number" LENGTH="15" NOTNULL="false" UNSIGNED="false" SEQUENCE="false" ENUM="false" DECIMALS="5" PREVIOUS="discriminationindex" NEXT="sd"/>
<FIELD NAME="sd" TYPE="number" LENGTH="15" NOTNULL="false" UNSIGNED="true" SEQUENCE="false" ENUM="false" DECIMALS="10" PREVIOUS="discriminativeefficiency" NEXT="facility"/>
<FIELD NAME="facility" TYPE="number" LENGTH="15" NOTNULL="false" UNSIGNED="true" SEQUENCE="false" ENUM="false" DECIMALS="10" PREVIOUS="sd" NEXT="subquestions"/>
<FIELD NAME="subquestions" TYPE="text" LENGTH="medium" NOTNULL="false" SEQUENCE="false" ENUM="false" PREVIOUS="facility"/>
<FIELD NAME="subquestions" TYPE="text" LENGTH="medium" NOTNULL="false" SEQUENCE="false" ENUM="false" PREVIOUS="facility" NEXT="maxgrade"/>
<FIELD NAME="maxgrade" TYPE="int" LENGTH="10" NOTNULL="false" UNSIGNED="true" SEQUENCE="false" ENUM="false" PREVIOUS="subquestions" NEXT="positions"/>
<FIELD NAME="positions" TYPE="text" LENGTH="medium" NOTNULL="false" SEQUENCE="false" ENUM="false" COMMENT="positions in which this item appears. Only used for random questions." PREVIOUS="maxgrade"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
......
......@@ -26,6 +26,32 @@ function xmldb_quizreport_statistics_upgrade($oldversion=0) {
}
}
if ($result && $oldversion < 2008072800) {
/// Define field maxgrade to be added to quiz_question_statistics
$table = new xmldb_table('quiz_question_statistics');
$field = new xmldb_field('maxgrade', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, null, null, null, null, null, 'subquestions');
/// Conditionally launch add field maxgrade
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
}
if ($result && $oldversion < 2008072801) {
/// Define field positions to be added to quiz_question_statistics
$table = new xmldb_table('quiz_question_statistics');
$field = new xmldb_field('positions', XMLDB_TYPE_TEXT, 'medium', null, null, null, null, null, null, 'maxgrade');
/// Conditionally launch add field positions
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
}
return $result;
}
......
......@@ -71,9 +71,11 @@ class qstats{
} else {
$stats->othergradesarray[] = $state->sumgrades;
}
}
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;
......@@ -84,23 +86,34 @@ class qstats{
$stats->covariancesum += $gradedifference * $othergradedifference;
$stats->covariancemaxsum += $sortedgradedifference * $sortedothergradedifference;
$stats->covariancewithoverallgradesum += $gradedifference * $overallgradedifference;
}
function _initial_question_walker(&$stats, $grade){
function _initial_question_walker(&$stats){
$stats->gradeaverage = $stats->totalgrades / $stats->s;
$stats->facility = $stats->gradeaverage / $grade;
$stats->facility = $stats->gradeaverage / $stats->maxgrade;
$stats->othergradeaverage = $stats->totalothergrades / $stats->s;
sort($stats->gradearray, SORT_NUMERIC);
sort($stats->othergradesarray, SORT_NUMERIC);
}
function _secondary_question_walker(&$stats){
$stats->gradevariance = $stats->gradevariancesum / ($stats->s -1);
$stats->othergradevariance = $stats->othergradevariancesum / ($stats->s -1);
$stats->covariance = $stats->covariancesum / ($stats->s -1);
$stats->covariancemax = $stats->covariancemaxsum / ($stats->s -1);
$stats->covariancewithoverallgrade = $stats->covariancewithoverallgradesum / ($stats->s-1);
$stats->sd = sqrt($stats->gradevariancesum / ($stats->s -1));
if ($stats->s > 1){
$stats->gradevariance = $stats->gradevariancesum / ($stats->s -1);
$stats->othergradevariance = $stats->othergradevariancesum / ($stats->s -1);
$stats->covariance = $stats->covariancesum / ($stats->s -1);
$stats->covariancemax = $stats->covariancemaxsum / ($stats->s -1);
$stats->covariancewithoverallgrade = $stats->covariancewithoverallgradesum / ($stats->s-1);
$stats->sd = sqrt($stats->gradevariancesum / ($stats->s -1));
} else {
$stats->gradevariance = null;
$stats->othergradevariance = null;
$stats->covariance = null;
$stats->covariancemax = null;
$stats->covariancewithoverallgrade = null;
$stats->sd = null;
}
//avoid divide by zero
if ($stats->gradevariance * $stats->othergradevariance){
$stats->discriminationindex = 100*$stats->covariance
......@@ -121,23 +134,24 @@ class qstats{
$this->_initial_states_walker($state, $this->questions[$state->question]->_stats);
//if this is a random question what is the real item being used?
if ($this->questions[$state->question]->qtype == 'random'){
if ($itemid = question_get_real_questionid($state)){
if (!isset($subquestionstats[$itemid])){
$subquestionstats[$itemid] = $this->stats_init_object();
$subquestionstats[$itemid]->usedin = array();
$subquestionstats[$itemid]->subquestion = true;
$subquestionstats[$itemid]->differentweights = false;
$subquestionstats[$itemid]->maxgrade = $this->questions[$state->question]->maxgrade;
} else if ($subquestionstats[$itemid]->maxgrade != $this->questions[$state->question]->maxgrade){
$subquestionstats[$itemid]->differentweights = true;
if ($realstate = question_get_real_state($state)){
if (!isset($subquestionstats[$realstate->question])){
$subquestionstats[$realstate->question] = $this->stats_init_object();
$subquestionstats[$realstate->question]->usedin = array();
$subquestionstats[$realstate->question]->subquestion = true;
$subquestionstats[$realstate->question]->differentweights = false;
$subquestionstats[$realstate->question]->maxgrade = $this->questions[$state->question]->maxgrade;
} else if ($subquestionstats[$realstate->question]->maxgrade != $this->questions[$state->question]->maxgrade){
$subquestionstats[$realstate->question]->differentweights = true;
}
$this->_initial_states_walker($state, $subquestionstats[$itemid], false);
$subquestionstats[$itemid]->usedin[$state->question] = $state->question;
$this->_initial_states_walker($realstate, $subquestionstats[$realstate->question], false);
$number = $this->questions[$state->question]->number;
$subquestionstats[$realstate->question]->usedin[$number] = $number;
$randomselectorstring = $this->questions[$state->question]->category.'/'.$this->questions[$state->question]->questiontext;
if (!isset($this->randomselectors[$randomselectorstring])){
$this->randomselectors[$randomselectorstring] = array();
}
$this->randomselectors[$randomselectorstring][$itemid] = $itemid;
$this->randomselectors[$randomselectorstring][$realstate->question] = $realstate->question;
}
}
}
......@@ -148,18 +162,25 @@ 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, $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));
}
if ($this->subquestions[$qid]->_stats->usedin){
sort($this->subquestions[$qid]->_stats->usedin, SORT_NUMERIC);
$this->subquestions[$qid]->_stats->positions = join($this->subquestions[$qid]->_stats->usedin, ',');
} else {
$this->subquestions[$qid]->_stats->positions = '';
}
}
reset($this->questions);
do{
list($qid, $question) = each($this->questions);
$nextquestion = current($this->questions);
$this->questions[$qid]->_stats->questionid = $qid;
$this->_initial_question_walker($this->questions[$qid]->_stats, $this->questions[$qid]->maxgrade);
$this->questions[$qid]->_stats->positions = $this->questions[$qid]->number;
$this->questions[$qid]->_stats->maxgrade = $question->maxgrade;
$this->_initial_question_walker($this->questions[$qid]->_stats);
if ($question->qtype == 'random'){
$randomselectorstring = $question->category.'/'.$question->questiontext;
if ($nextquestion){
......@@ -177,8 +198,8 @@ class qstats{
foreach ($this->states as $state){
$this->_secondary_states_walker($state, $this->questions[$state->question]->_stats);
if ($this->questions[$state->question]->qtype == 'random'){
if ($itemid = question_get_real_questionid($state)){
$this->_secondary_states_walker($state, $this->subquestions[$itemid]->_stats);
if ($realstate = question_get_real_state($state)){
$this->_secondary_states_walker($realstate, $this->subquestions[$realstate->question]->_stats);
}
}
}
......@@ -192,8 +213,12 @@ class qstats{
$this->_secondary_question_walker($this->subquestions[$qid]->_stats);
}
foreach (array_keys($this->questions) as $qid){
$this->questions[$qid]->_stats->effectiveweight = 100 * sqrt($this->questions[$qid]->_stats->covariancewithoverallgrade)
/ $sumofcovariancewithoverallgrade;
if ($sumofcovariancewithoverallgrade){
$this->questions[$qid]->_stats->effectiveweight = 100 * sqrt($this->questions[$qid]->_stats->covariancewithoverallgrade)
/ $sumofcovariancewithoverallgrade;
} else {
$this->questions[$qid]->_stats->effectiveweight = null;
}
}
}
/**
......
This diff is collapsed.
<?php // $Id$
include '../../../../config.php';
include $CFG->dirroot."/lib/graphlib.php";
include $CFG->dirroot."/mod/quiz/locallib.php";
include $CFG->dirroot."/mod/quiz/report/reportlib.php";
function graph_get_new_colour(){
static $colourindex = 0;
$colours = array('red', 'green', 'yellow', 'orange', 'purple', 'black', 'maroon', 'blue', 'ltgreen', 'navy', 'ltred', 'ltltgreen', 'ltltorange', 'olive', 'gray', 'ltltred', 'ltorange', 'lime', 'ltblue', 'ltltblue');
$colour = $colours[$colourindex];
$colourindex++;
if ($colourindex > (count($colours)-1)){
$colourindex =0;
}
return $colour;
}
$quizstatisticsid = required_param('id', PARAM_INT);
$quizstatistics = $DB->get_record('quiz_statistics', array('id' => $quizstatisticsid));
$questionstatistics = $DB->get_records('quiz_question_statistics', array('quizstatisticsid' => $quizstatistics->id, 'subquestion' => 0));
$quiz = $DB->get_record('quiz', array('id' => $quizstatistics->quizid));
require_login($quiz->course);
$questions = quiz_report_load_questions($quiz);
$cm = get_coursemodule_from_instance('quiz', $quiz->id);
if ($groupmode = groups_get_activity_groupmode($cm)) { // Groups are being used
$groups = groups_get_activity_allowed_groups($cm);
} else {
$groups = false;
}
if ($quizstatistics->groupid){
if (!in_array($quizstatistics->groupid, $groups)){
print_error('groupnotamember', 'group');
}
}
$modcontext = get_context_instance(CONTEXT_MODULE, $cm->id);
require_capability('mod/quiz:viewreports', $modcontext);
$line = new graph(800,600);
$line->parameter['title'] = '';
$line->parameter['y_label_left'] = '%';
$line->parameter['x_label'] = get_string('position', 'quiz_statistics');
$line->parameter['y_label_angle'] = 90;
$line->parameter['x_label_angle'] = 0;
$line->parameter['x_axis_angle'] = 60;
$line->parameter['legend'] = 'outside-top';
$line->parameter['legend_border'] = 'black';
$line->parameter['legend_offset'] = 4;
$line->parameter['bar_size'] = 1.2;
$line->parameter['bar_spacing'] = 10;
$fieldstoplot = array('facility' => get_string('facility', 'quiz_statistics'), 'discriminativeefficiency' => get_string('discriminative_efficiency', 'quiz_statistics'));
$fieldstoplotfactor = array('facility' => 100, 'discriminativeefficiency' => 1);
$line->x_data = array();
foreach (array_keys($fieldstoplot) as $fieldtoplot){
$line->y_data[$fieldtoplot] = array();
$line->y_format[$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;
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;
}
}
ksort($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;
}
$line->y_order = array_keys($fieldstoplot);
$line->parameter['y_min_left'] = $min; // start at 0
$line->parameter['y_max_left'] = $max;
$line->parameter['y_decimal_left'] = 0;
$line->draw();
?>
......@@ -59,7 +59,7 @@ class quiz_report_statistics_table extends flexible_table {
$columns[]= 'discriminative_efficiency';
$headers[]= get_string('discriminative_efficiency', 'quiz_statistics');
$this->define_columns($columns);
$this->define_headers($headers);
$this->sortable(false);
......@@ -99,7 +99,13 @@ class quiz_report_statistics_table extends flexible_table {
function col_name($question){
return $question->name;