From: jamiesensei Date: Wed, 30 Jul 2008 09:02:44 +0000 (+0000) Subject: MDL-15268 "Content for Quiz Statistics report table" further work on quiz statistics... X-Git-Url: http://git.mjollnir.org/gw?a=commitdiff_plain;h=43ec99aa74f9a95a55766de58e0a59c1df038a30;p=moodle.git MDL-15268 "Content for Quiz Statistics report table" further work on quiz statistics report. --- diff --git a/lang/en_utf8/quiz_statistics.php b/lang/en_utf8/quiz_statistics.php index 88f0f8b97f..2dc0dd77fa 100644 --- a/lang/en_utf8/quiz_statistics.php +++ b/lang/en_utf8/quiz_statistics.php @@ -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 diff --git a/lib/questionlib.php b/lib/questionlib.php index ae735b681e..a041b857f3 100644 --- a/lib/questionlib.php +++ b/lib/questionlib.php @@ -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; } } + ?> diff --git a/lib/tablelib.php b/lib/tablelib.php index e682d02898..0da006d915 100644 --- a/lib/tablelib.php +++ b/lib/tablelib.php @@ -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 << */ - - $sheettitle - -

$sheettitle

EOF; + $this->documentstarted = true; + } + function start_table($sheettitle){ + $this->table->sortable(false); + $this->table->collapsible(false); + echo "

{$sheettitle}

"; $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 ''; exit; } diff --git a/mod/quiz/report/reportlib.php b/mod/quiz/report/reportlib.php index 41b50f3ede..417b71dd2f 100644 --- a/mod/quiz/report/reportlib.php +++ b/mod/quiz/report/reportlib.php @@ -312,6 +312,6 @@ function quiz_report_scale_sumgrades_as_percentage($rawgrade, $quiz, $round = tr } else { $grade = 0; } - return $grade.' %'; + return $grade.'%'; } ?> diff --git a/mod/quiz/report/statistics/cron.php b/mod/quiz/report/statistics/cron.php index 0c4537b04b..c6f6aab077 100644 --- a/mod/quiz/report/statistics/cron.php +++ b/mod/quiz/report/statistics/cron.php @@ -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.'); } diff --git a/mod/quiz/report/statistics/db/install.xml b/mod/quiz/report/statistics/db/install.xml index 1410e39837..89f61b7098 100644 --- a/mod/quiz/report/statistics/db/install.xml +++ b/mod/quiz/report/statistics/db/install.xml @@ -1,5 +1,5 @@ - @@ -39,7 +39,9 @@ - + + + diff --git a/mod/quiz/report/statistics/db/upgrade.php b/mod/quiz/report/statistics/db/upgrade.php index b2893f9584..10489e9b65 100644 --- a/mod/quiz/report/statistics/db/upgrade.php +++ b/mod/quiz/report/statistics/db/upgrade.php @@ -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; } diff --git a/mod/quiz/report/statistics/qstats.php b/mod/quiz/report/statistics/qstats.php index 3177e2ba82..aeca387e9b 100644 --- a/mod/quiz/report/statistics/qstats.php +++ b/mod/quiz/report/statistics/qstats.php @@ -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; + } } } /** diff --git a/mod/quiz/report/statistics/report.php b/mod/quiz/report/statistics/report.php index fab9d3a889..3f9ad7556c 100644 --- a/mod/quiz/report/statistics/report.php +++ b/mod/quiz/report/statistics/report.php @@ -13,6 +13,11 @@ require_once($CFG->dirroot.'/mod/quiz/report/statistics/statistics_form.php'); require_once($CFG->dirroot.'/mod/quiz/report/statistics/statistics_table.php'); class quiz_statistics_report extends quiz_default_report { + + /** + * @var object instance of table class used for main questions stats table. + */ + var $table; /** * Display the report. @@ -24,6 +29,8 @@ class quiz_statistics_report extends quiz_default_report { $download = optional_param('download', '', PARAM_ALPHA); $recalculate = optional_param('recalculate', 0, PARAM_BOOL); + //pass the question id for detailed analysis question + $qid = optional_param('qid', 0, PARAM_INT); $pageoptions = array(); $pageoptions['id'] = $cm->id; $pageoptions['q'] = $quiz->id; @@ -52,6 +59,7 @@ class quiz_statistics_report extends quiz_default_report { /// find out current groups mode $currentgroup = groups_get_activity_group($cm, true); + $nostudentsingroup = false;//true if a group is selected and their is noeone in it. if (!empty($currentgroup)) { @@ -63,132 +71,196 @@ class quiz_statistics_report extends quiz_default_report { } else { $groupstudents = array(); } - - $table = new quiz_report_statistics_table(); - $table->is_downloading($download, get_string('reportstatistics','quiz_statistics'), - "$course->shortname ".format_string($quiz->name,true)); - if (!$table->is_downloading()) { + if ($recalculate){ + 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'); + } + if (!$DB->delete_records_select('quiz_question_statistics', "quizstatisticsid $todeletesql", $todeleteparams)){ + print_error('errordeletingqstats', 'quiz_statistics'); + } + } + redirect($reporturl->out()); + } + + + $this->table = new quiz_report_statistics_table(); + $filename = "$course->shortname-".format_string($quiz->name,true); + $this->table->is_downloading($download, $filename, get_string('quizstructureanalysis', 'quiz_statistics')); + if (!$this->table->is_downloading()) { // Only print headers if not asked to download data $this->print_header_and_tabs($cm, $course, $quiz, "statistics"); } if ($groupmode = groups_get_activity_groupmode($cm)) { // Groups are being used - if (!$table->is_downloading()) { + if (!$this->table->is_downloading()) { groups_print_activity_menu($cm, $reporturl->out()); + echo '
'; if ($currentgroup && !$groupstudents){ notify(get_string('nostudentsingroup', 'quiz_statistics')); } } } - if (!$table->is_downloading()) { + if (!$this->table->is_downloading()) { // Print display options $mform->set_data(array('useallattempts' => $useallattempts)); $mform->display(); } - // Print information on the number of existing attempts - if (!$table->is_downloading()) { //do not print notices when downloading - print_heading(get_string('quizinformation', 'quiz_statistics')); - $quizinformationtable = new object(); - $quizinformationtable->align = array('center', 'center'); - $quizinformationtable->width = '60%'; - $quizinformationtable->class = 'generaltable titlesleft'; - $quizinformationtable->data = array(); - $quizinformationtable->data[] = array(get_string('quizname', 'quiz_statistics'), $quiz->name); - $quizinformationtable->data[] = array(get_string('coursename', 'quiz_statistics'), $course->fullname); - if ($cm->idnumber){ - $quizinformationtable->data[] = array(get_string('coursename', 'quiz_statistics'), $cm->idnumber); - } - if ($quiz->timeopen){ - $quizinformationtable->data[] = array(get_string('quizopen', 'quiz'), userdate($quiz->timeopen)); - } - if ($quiz->timeclose){ - $quizinformationtable->data[] = array(get_string('quizclose', 'quiz'), userdate($quiz->timeclose)); - } - if ($quiz->timeopen && $quiz->timeclose){ - $quizinformationtable->data[] = array(get_string('duration', 'quiz_statistics'), format_time($quiz->timeclose - $quiz->timeopen)); - } - } - $timemodified = time() - QUIZ_REPORT_TIME_TO_CACHE_STATS; - $params = array('quizid'=>$quiz->id, 'groupid'=>$currentgroup, 'allattempts'=>$useallattempts, 'timemodified'=>$timemodified); - if ($recalculate || !$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(); - $quizstatisticsid = $DB->insert_record('quiz_statistics', $toinsert); - foreach ($qstats->questions as $question){ - $question->_stats->quizstatisticsid = $quizstatisticsid; - $DB->insert_record('quiz_question_statistics', $question->_stats, false, true); - } - foreach ($qstats->subquestions as $subquestion){ - $subquestion->_stats->quizstatisticsid = $quizstatisticsid; - $DB->insert_record('quiz_question_statistics', $subquestion->_stats, false, true); - } - if (isset($qstats)){ - $questions = $qstats->questions; - $subquestions = $qstats->subquestions; - } else { - $questions = array(); - $subquestions = array(); + list($quizstats, $questions, $subquestions, $s, $usingattemptsstring) + = $this->quiz_questions_stats($quiz, $currentgroup, $nostudentsingroup, + $useallattempts, $groupstudents, $questions); + + if (!$this->table->is_downloading()){ + if ($s==0){ + print_heading(get_string('noattempts','quiz')); } + } + if ($s){ + $this->table->setup($quiz, $cm->id, $reporturl, $s); + } + + if (!$qid){ + $this->output_quiz_stats_table($course, $cm, $quiz, $quizstats, $usingattemptsstring, $currentgroup, $groupstudents, $useallattempts, $download, $reporturl); + $this->output_question_stats_table($s, $questions, $subquestions); } else { - if ($useallattempts){ - $usingattemptsstring = get_string('allattempts', 'quiz_statistics'); - $s = $quizstats->allattemptscount; + $thisquestion = false; + if (isset($questions[$qid])){ + $thisquestion = $questions[$qid]; + } else if (isset($subquestions[$qid])){ + $thisquestion = $subquestions[$qid]; } else { - $usingattemptsstring = get_string('firstattempts', 'quiz_statistics'); - $s = $quizstats->firstattemptscount; + print_error('questiondoesnotexist', 'question'); } - $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)){ - list($mainquestionstats, $subquestionstats) = $questionstats; - $subqstofetch = array_keys($subquestionstats); - $subquestions = question_load_questions($subqstofetch); - foreach (array_keys($subquestions) as $subqid){ - $subquestions[$subqid]->_stats = $subquestionstats[$subqid]; + $this->output_question_info_table($quiz, $thisquestion); + } + 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.' '.$datumfromtable['actions']); + $questioninfotable->data[] = array(get_string('questiontype', 'quiz_statistics'), $question->qtype.' '.$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); + + print_heading(get_string('analysisofresponses', 'quiz_statistics')); + + } + + function output_question_stats_table($s, $questions, $subquestions){ + if (!$this->table->is_downloading()){ + print_heading(get_string('quizstructureanalysis', 'quiz_statistics')); + } + if ($s){ + foreach ($questions as $question){ + $this->table->add_data_keyed($this->table->format_row($question)); + if (!empty($question->_stats->subquestions)){ + $subitemstodisplay = explode(',', $question->_stats->subquestions); + foreach ($subitemstodisplay as $subitemid){ + $subquestions[$subitemid]->maxgrade = $question->maxgrade; + $this->table->add_data_keyed($this->table->format_row($subquestions[$subitemid])); + } } - } else { - $mainquestionstats = $questionstats[0]; - $subquestions = array(); - } - foreach (array_keys($questions) as $qid){ - $questions[$qid]->_stats = $mainquestionstats[$qid]; } + + $this->table->finish_output(); } - if (!$table->is_downloading()){ - if ($s==0){ - print_heading(get_string('noattempts','quiz')); + } + + function output_quiz_stats_table($course, $cm, $quiz, $quizstats, $usingattemptsstring, $currentgroup, $groupstudents, $useallattempts, $download, $reporturl){ + global $DB; + // Print information on the number of existing attempts + $quizinformationtablehtml = print_heading(get_string('quizinformation', 'quiz_statistics'), '', 2, 'main', true); + $quizinformationtable = new object(); + $quizinformationtable->align = array('center', 'center'); + $quizinformationtable->width = '60%'; + $quizinformationtable->class = 'generaltable titlesleft'; + $quizinformationtable->data = array(); + $quizinformationtable->data[] = array(get_string('quizname', 'quiz_statistics'), $quiz->name); + $quizinformationtable->data[] = array(get_string('coursename', 'quiz_statistics'), $course->fullname); + if ($cm->idnumber){ + $quizinformationtable->data[] = array(get_string('coursename', 'quiz_statistics'), $cm->idnumber); + } + if ($quiz->timeopen){ + $quizinformationtable->data[] = array(get_string('quizopen', 'quiz'), userdate($quiz->timeopen)); + } + if ($quiz->timeclose){ + $quizinformationtable->data[] = array(get_string('quizclose', 'quiz'), userdate($quiz->timeclose)); + } + if ($quiz->timeopen && $quiz->timeclose){ + $quizinformationtable->data[] = array(get_string('duration', 'quiz_statistics'), format_time($quiz->timeclose - $quiz->timeopen)); + } + $format = array('firstattemptscount' => '', + 'allattemptscount' => '', + 'firstattemptsavg' => 'sumgrades_as_percentage', + 'allattemptsavg' => 'sumgrades_as_percentage', + 'median' => 'sumgrades_as_percentage', + 'standarddeviation' => 'sumgrades_as_percentage', + 'skewness' => '', + 'kurtosis' => '', + 'cic' => 'number_format', + 'errorratio' => 'number_format', + 'standarderror' => 'sumgrades_as_percentage'); + foreach ($quizstats as $property => $value){ + if (!isset($format[$property])){ + continue; } - $format = array('firstattemptscount' => '', - 'allattemptscount' => '', - 'firstattemptsavg' => 'sumgrades_as_percentage', - 'allattemptsavg' => 'sumgrades_as_percentage', - 'median' => 'sumgrades_as_percentage', - 'standarddeviation' => 'sumgrades_as_percentage', - 'skewness' => '', - 'kurtosis' => '', - 'cic' => 'number_format', - 'errorratio' => 'number_format', - 'standarderror' => 'sumgrades_as_percentage'); - foreach ($quizstats as $property => $value){ - if (!isset($format[$property])){ - continue; - } + if (!is_null($value)){ switch ($format[$property]){ case 'sumgrades_as_percentage' : $formattedvalue = quiz_report_scale_sumgrades_as_percentage($value, $quiz); break; case 'number_format' : - $formattedvalue = number_format($value, $quiz->decimalpoints).' %'; + $formattedvalue = number_format($value, $quiz->decimalpoints).'%'; break; default : $formattedvalue = $value; } $quizinformationtable->data[] = array(get_string($property, 'quiz_statistics', $usingattemptsstring), $formattedvalue); } + } + if (!$this->table->is_downloading()){ if (isset($quizstats->timemodified)){ list($fromqa, $whereqa, $qaparams) = quiz_report_attempts_sql($quiz->id, $currentgroup, $groupstudents, $useallattempts); $sql = 'SELECT COUNT(1) ' . @@ -198,39 +270,36 @@ class quiz_statistics_report extends quiz_default_report { $a->lastcalculated = format_time(time() - $quizstats->timemodified); if (!$a->count = $DB->count_records_sql($sql, array('time'=>$quizstats->timemodified)+$qaparams)){ $a->count = 0; - } - print_box_start('boxaligncenter generalbox boxwidthnormal mdl-align'); - echo get_string('lastcalculated', 'quiz_statistics', $a); - print_single_button($reporturl->out(true), $reporturl->params()+array('recalculate'=>1), - get_string('recalculatenow', 'quiz_statistics'), 'post'); - print_box_end(); + } + $quizinformationtablehtml .= print_box_start('boxaligncenter generalbox boxwidthnormal mdl-align', '', true); + $quizinformationtablehtml .= get_string('lastcalculated', 'quiz_statistics', $a); + $quizinformationtablehtml .= print_single_button($reporturl->out(true), $reporturl->params()+array('recalculate'=>1), + get_string('recalculatenow', 'quiz_statistics'), 'post', '', true); + $quizinformationtablehtml .= print_box_end(true); } - print_table($quizinformationtable); - } - if (!$table->is_downloading()){ - print_heading(get_string('quizstructureanalysis', 'quiz_statistics')); - } - if ($s){ - $table->setup($quiz, $cm->id, $reporturl, $s); - - foreach ($questions as $question){ - $table->add_data_keyed($table->format_row($question)); - if (!empty($question->_stats->subquestions)){ - $subitemstodisplay = explode(',', $question->_stats->subquestions); - foreach ($subitemstodisplay as $subitemid){ - $subquestions[$subitemid]->maxgrade = $question->maxgrade; - $table->add_data_keyed($table->format_row($subquestions[$subitemid])); - } + $quizinformationtablehtml .= print_table($quizinformationtable, true); + if (!$this->table->is_downloading()){ + echo $quizinformationtablehtml; + } else { + $exportclass =& $this->table->export_class_instance(); + if ($download == 'xhtml'){ + echo $quizinformationtablehtml; + } else { + $exportclass->start_table(get_string('quizinformation', 'quiz_statistics')); + $headers = array(); + $row = array(); + foreach ($quizinformationtable->data as $data){ + $headers[]= $data[0]; + $row[] = $data[1]; } + $exportclass->output_headers($headers); + $exportclass->add_data($row); + $exportclass->finish_table(); } - - $table->finish_output(); } - return true; } - - + function quiz_stats($nostudentsingroup, $quizid, $currentgroup, $groupstudents, $questions, $useallattempts){ global $CFG, $DB; if (!$nostudentsingroup){ @@ -337,15 +406,17 @@ class quiz_statistics_report extends quiz_default_report { $k2= $s*$m2/($s-1); $k3= $s*$s*$m3/(($s-1)*($s-2)); - - $quizstats->skewness = $k3 / (pow($k2, 2/3)); + if ($k2){ + $quizstats->skewness = $k3 / (pow($k2, 2/3)); + } } if ($s>3){ $k4= (($s*$s*$s)/(($s-1)*($s-2)*($s-3)))*((($s+1)*$m4)-(3*($s-1)*$m2*$m2)); - - $quizstats->kurtosis = $k4 / ($k2*$k2); + if ($k2){ + $quizstats->kurtosis = $k4 / ($k2*$k2); + } } } } @@ -353,7 +424,6 @@ class quiz_statistics_report extends quiz_default_report { require_once("$CFG->dirroot/mod/quiz/report/statistics/qstats.php"); $qstats = new qstats($questions, $s, $sumgradesavg); $qstats->get_records($quizid, $currentgroup, $groupstudents, $useallattempts); - set_time_limit(0); $qstats->process_states(); } else { $qstats = false; @@ -361,15 +431,68 @@ class quiz_statistics_report extends quiz_default_report { if ($s>1){ $p = count($qstats->questions);//no of positions if ($p > 1){ - $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); - + if ($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); + } } } return array($s, $usingattemptsstring, $quizstats, $qstats); } - + + function quiz_questions_stats($quiz, $currentgroup, $nostudentsingroup, $useallattempts, $groupstudents, $questions){ + global $DB; + $timemodified = time() - QUIZ_REPORT_TIME_TO_CACHE_STATS; + $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(); + $quizstatisticsid = $DB->insert_record('quiz_statistics', $toinsert); + foreach ($qstats->questions as $question){ + $question->_stats->quizstatisticsid = $quizstatisticsid; + $DB->insert_record('quiz_question_statistics', $question->_stats, false, true); + } + foreach ($qstats->subquestions as $subquestion){ + $subquestion->_stats->quizstatisticsid = $quizstatisticsid; + $DB->insert_record('quiz_question_statistics', $subquestion->_stats, false, true); + } + if (isset($qstats)){ + $questions = $qstats->questions; + $subquestions = $qstats->subquestions; + } else { + $questions = array(); + $subquestions = array(); + } + } else { + //use cached results + if ($useallattempts){ + $usingattemptsstring = get_string('allattempts', 'quiz_statistics'); + $s = $quizstats->allattemptscount; + } else { + $usingattemptsstring = get_string('firstattempts', 'quiz_statistics'); + $s = $quizstats->firstattemptscount; + } + $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)){ + list($mainquestionstats, $subquestionstats) = $questionstats; + $subqstofetch = array_keys($subquestionstats); + $subquestions = question_load_questions($subqstofetch); + foreach (array_keys($subquestions) as $subqid){ + $subquestions[$subqid]->_stats = $subquestionstats[$subqid]; + } + } else { + $mainquestionstats = $questionstats[0]; + $subquestions = array(); + } + foreach (array_keys($questions) as $qid){ + $questions[$qid]->_stats = $mainquestionstats[$qid]; + } + } + return array($quizstats, $questions, $subquestions, $s, $usingattemptsstring); + } } function quiz_report_attempts_sql($quizid, $currentgroup, $groupstudents, $allattempts = true){ global $DB; @@ -386,4 +509,10 @@ 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; +} ?> diff --git a/mod/quiz/report/statistics/statistics_graph.php b/mod/quiz/report/statistics/statistics_graph.php new file mode 100644 index 0000000000..4d906b0e77 --- /dev/null +++ b/mod/quiz/report/statistics/statistics_graph.php @@ -0,0 +1,88 @@ +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(); +?> diff --git a/mod/quiz/report/statistics/statistics_table.php b/mod/quiz/report/statistics/statistics_table.php index a4cdb1c694..366aefc1eb 100644 --- a/mod/quiz/report/statistics/statistics_table.php +++ b/mod/quiz/report/statistics/statistics_table.php @@ -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; + if (!$this->is_downloading() && $question->qtype!='random'){ + $tooltip = get_string('detailedanalysis', 'quiz_statistics'); + $url = $this->baseurl .'&qid='.$question->id; + return "".$question->name.""; + } else { + return $question->name; + } } @@ -121,25 +127,25 @@ class quiz_report_statistics_table extends flexible_table { return $question->qtype; } function col_intended_weight($question){ - return quiz_report_scale_sumgrades_as_percentage($question->maxgrade, $this->quiz); + return quiz_report_scale_sumgrades_as_percentage($question->_stats->maxgrade, $this->quiz); } function col_effective_weight($question){ if (!$question->_stats->subquestion){ - return number_format($question->_stats->effectiveweight, 2).' %'; + return number_format($question->_stats->effectiveweight, 2).'%'; } else { return ''; } } function col_discrimination_index($question){ if (is_numeric($question->_stats->discriminationindex)){ - return number_format($question->_stats->discriminationindex, 2).' %'; + return number_format($question->_stats->discriminationindex, 2).'%'; } else { return $question->_stats->discriminationindex; } } function col_discriminative_efficiency($question){ if (is_numeric($question->_stats->discriminativeefficiency)){ - return number_format($question->_stats->discriminativeefficiency, 2).' %'; + return number_format($question->_stats->discriminativeefficiency, 2).'%'; } else { return ''; } @@ -147,14 +153,14 @@ class quiz_report_statistics_table extends flexible_table { function col_random_guess_score($question){ $randomguessscore = question_get_random_guess_score($question); if (is_numeric($randomguessscore)){ - return number_format($randomguessscore * 100, 2).' %'; + return number_format($randomguessscore * 100, 2).'%'; } else { return $randomguessscore; // empty string returned by random question. } } function col_sd($question){ - return number_format($question->_stats->sd*100 / $question->maxgrade, 2).' %'; + return number_format($question->_stats->sd*100 / $question->_stats->maxgrade, 2).'%'; } function col_s($question){ if (isset($question->_stats->s)){ @@ -164,7 +170,8 @@ class quiz_report_statistics_table extends flexible_table { } } function col_facility($question){ - return number_format($question->_stats->facility*100, 2).' %'; + return number_format($question->_stats->facility*100, 2).'%'; } + } ?> diff --git a/mod/quiz/report/statistics/version.php b/mod/quiz/report/statistics/version.php index b522d2d478..406f977934 100644 --- a/mod/quiz/report/statistics/version.php +++ b/mod/quiz/report/statistics/version.php @@ -1,4 +1,4 @@ version = 2008072500; // The (date) version of this module +$plugin->version = 2008072801; // The (date) version of this module ?> \ No newline at end of file