Commit 869309b8 authored by jamiesensei's avatar jamiesensei
Browse files

MDL-14202 "Replace Item Analysis Report with new improved 'Statistics'...

MDL-14202 "Replace Item Analysis Report with new improved 'Statistics' report." finished statistics report. This patch includes some changes to lib/tablelib.php so that it is possible to export the content of a table as part of a multi table export - with mutliple tables / multiple worksheets.
parent c861c8ac
......@@ -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
......@@ -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;
}
}
......
......@@ -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.'%';
}
......
<?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"/>
......@@ -47,5 +47,20 @@
<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
......@@ -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;
}
......
......@@ -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.
......
This diff is collapsed.
......@@ -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;
......
<?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 '';
}
}
}
?>
......@@ -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);
......
<?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
......@@ -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;
......
......@@ -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;
}