From: jamiesensei Date: Thu, 11 Sep 2008 12:48:08 +0000 (+0000) Subject: MDL-14202 "Replace Item Analysis Report with new improved 'Statistics' report." finis... X-Git-Url: http://git.mjollnir.org/gw?a=commitdiff_plain;h=869309b8d8ba9548e11fe06295cc7ac6c0ccd453;p=moodle.git 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. --- diff --git a/lang/en_utf8/quiz_statistics.php b/lang/en_utf8/quiz_statistics.php index 2642cec492..1a6ff7d356 100644 --- a/lang/en_utf8/quiz_statistics.php +++ b/lang/en_utf8/quiz_statistics.php @@ -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 diff --git a/lib/tablelib.php b/lib/tablelib.php index bdca620a86..9871f49f6e 100644 --- a/lib/tablelib.php +++ b/lib/tablelib.php @@ -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{ + +$filename + EOF; $this->documentstarted = true; @@ -1491,7 +1500,7 @@ EOF; $this->table->finish_html(); } function finish_document(){ - echo ''; + echo "\n"; exit; } } diff --git a/mod/quiz/report/reportlib.php b/mod/quiz/report/reportlib.php index d81282e489..4d99e07b6a 100644 --- a/mod/quiz/report/reportlib.php +++ b/mod/quiz/report/reportlib.php @@ -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.'%'; } diff --git a/mod/quiz/report/statistics/db/install.xml b/mod/quiz/report/statistics/db/install.xml index 13cb5fd317..08d69ea025 100644 --- a/mod/quiz/report/statistics/db/install.xml +++ b/mod/quiz/report/statistics/db/install.xml @@ -1,5 +1,5 @@ - @@ -27,7 +27,7 @@ - +
@@ -47,5 +47,20 @@
+ + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/mod/quiz/report/statistics/db/upgrade.php b/mod/quiz/report/statistics/db/upgrade.php index d6ec6023a8..4b65ff2c74 100644 --- a/mod/quiz/report/statistics/db/upgrade.php +++ b/mod/quiz/report/statistics/db/upgrade.php @@ -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; } diff --git a/mod/quiz/report/statistics/qstats.php b/mod/quiz/report/statistics/qstats.php index aeca387e9b..15dfe71e98 100644 --- a/mod/quiz/report/statistics/qstats.php +++ b/mod/quiz/report/statistics/qstats.php @@ -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. diff --git a/mod/quiz/report/statistics/report.php b/mod/quiz/report/statistics/report.php index 294c095698..610c1e3e21 100644 --- a/mod/quiz/report/statistics/report.php +++ b/mod/quiz/report/statistics/report.php @@ -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 '
'.get_string('statisticsreportgraph', 'quiz_statistics').'
'; + $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 '
'.get_string('statisticsreportgraph', 'quiz_statistics').'
'; + } + } + 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.' '.$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); + 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.' '.$datumfromtable['actions']); + $questioninfotable->data[] = array(get_string('questiontype', 'quiz_statistics'), $datumfromtable['icon'].' '.get_string($question->qtype,'quiz').' '.$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 = "$questiontabletitle"; + 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 = '    '; + } 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("$text", '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 .= '
'; + $quizinformationtablehtml .= '
'; + $quizinformationtablehtml .= ''; + $quizinformationtablehtml .= ''; + $quizinformationtablehtml .= choose_from_menu ($downloadoptions, 'download', $this->table->defaultdownloadformat, '', '', '', true); + $quizinformationtablehtml .= helpbutton('tableexportformats', get_string('tableexportformats', 'table'), 'moodle', true, false, '', true); + $quizinformationtablehtml .= '
'; } $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; -} + ?> diff --git a/mod/quiz/report/statistics/statistics_graph.php b/mod/quiz/report/statistics/statistics_graph.php index 4d906b0e77..15589e57d2 100644 --- a/mod/quiz/report/statistics/statistics_graph.php +++ b/mod/quiz/report/statistics/statistics_graph.php @@ -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 index 0000000000..aeb3cc39bc --- /dev/null +++ b/mod/quiz/report/statistics/statistics_question_table.php @@ -0,0 +1,83 @@ +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 ''; + } + } + + + +} +?> diff --git a/mod/quiz/report/statistics/statistics_table.php b/mod/quiz/report/statistics/statistics_table.php index e2a33acb0d..d1d39552cd 100644 --- a/mod/quiz/report/statistics/statistics_table.php +++ b/mod/quiz/report/statistics/statistics_table.php @@ -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); diff --git a/mod/quiz/report/statistics/version.php b/mod/quiz/report/statistics/version.php index 4e37fd381a..e08b511b26 100644 --- a/mod/quiz/report/statistics/version.php +++ b/mod/quiz/report/statistics/version.php @@ -1,4 +1,4 @@ version = 2008081500; // The (date) version of this module +$plugin->version = 2008090500; // The (date) version of this module ?> \ No newline at end of file diff --git a/question/type/calculated/questiontype.php b/question/type/calculated/questiontype.php index 11250055a5..b71606231a 100644 --- a/question/type/calculated/questiontype.php +++ b/question/type/calculated/questiontype.php @@ -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; diff --git a/question/type/match/questiontype.php b/question/type/match/questiontype.php index 7efdf32a1b..b5d62c89aa 100644 --- a/question/type/match/questiontype.php +++ b/question/type/match/questiontype.php @@ -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); } diff --git a/question/type/multianswer/questiontype.php b/question/type/multianswer/questiontype.php index 03f2ec17e7..adab307b07 100644 --- a/question/type/multianswer/questiontype.php +++ b/question/type/multianswer/questiontype.php @@ -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; diff --git a/question/type/numerical/questiontype.php b/question/type/numerical/questiontype.php index 706a3cf316..00d31d146c 100644 --- a/question/type/numerical/questiontype.php +++ b/question/type/numerical/questiontype.php @@ -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 diff --git a/question/type/questiontype.php b/question/type/questiontype.php index 72ef1a85d0..9610c11211 100644 --- a/question/type/questiontype.php +++ b/question/type/questiontype.php @@ -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; diff --git a/question/type/randomsamatch/questiontype.php b/question/type/randomsamatch/questiontype.php index 0879a67c38..56fb4d36ab 100644 --- a/question/type/randomsamatch/questiontype.php +++ b/question/type/randomsamatch/questiontype.php @@ -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 diff --git a/question/type/shortanswer/questiontype.php b/question/type/shortanswer/questiontype.php index a01d1da30c..cdf6e149db 100644 --- a/question/type/shortanswer/questiontype.php +++ b/question/type/shortanswer/questiontype.php @@ -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