|
Moodle
2.2.1
http://www.collinsharper.com
|
00001 <?php 00002 // This file is part of Moodle - http://moodle.org/ 00003 // 00004 // Moodle is free software: you can redistribute it and/or modify 00005 // it under the terms of the GNU General Public License as published by 00006 // the Free Software Foundation, either version 3 of the License, or 00007 // (at your option) any later version. 00008 // 00009 // Moodle is distributed in the hope that it will be useful, 00010 // but WITHOUT ANY WARRANTY; without even the implied warranty of 00011 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 00012 // GNU General Public License for more details. 00013 // 00014 // You should have received a copy of the GNU General Public License 00015 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 00016 00027 defined('MOODLE_INTERNAL') || die(); 00028 00029 require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_form.php'); 00030 require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_table.php'); 00031 require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_question_table.php'); 00032 require_once($CFG->dirroot . '/mod/quiz/report/statistics/qstats.php'); 00033 require_once($CFG->dirroot . '/mod/quiz/report/statistics/responseanalysis.php'); 00034 00035 00044 class quiz_statistics_report extends quiz_default_report { 00046 const TIME_TO_CACHE_STATS = 900; // 15 minutes 00047 00049 protected $table; 00050 00054 public function display($quiz, $cm, $course) { 00055 global $CFG, $DB, $OUTPUT, $PAGE; 00056 00057 $this->context = get_context_instance(CONTEXT_MODULE, $cm->id); 00058 00059 // Work out the display options. 00060 $download = optional_param('download', '', PARAM_ALPHA); 00061 $everything = optional_param('everything', 0, PARAM_BOOL); 00062 $recalculate = optional_param('recalculate', 0, PARAM_BOOL); 00063 // A qid paramter indicates we should display the detailed analysis of a question. 00064 $qid = optional_param('qid', 0, PARAM_INT); 00065 $slot = optional_param('slot', 0, PARAM_INT); 00066 00067 $pageoptions = array(); 00068 $pageoptions['id'] = $cm->id; 00069 $pageoptions['mode'] = 'statistics'; 00070 00071 $reporturl = new moodle_url('/mod/quiz/report.php', $pageoptions); 00072 00073 $mform = new quiz_statistics_statistics_settings_form($reporturl); 00074 if ($fromform = $mform->get_data()) { 00075 $useallattempts = $fromform->useallattempts; 00076 if ($fromform->useallattempts) { 00077 set_user_preference('quiz_report_statistics_useallattempts', 00078 $fromform->useallattempts); 00079 } else { 00080 unset_user_preference('quiz_report_statistics_useallattempts'); 00081 } 00082 00083 } else { 00084 $useallattempts = get_user_preferences('quiz_report_statistics_useallattempts', 0); 00085 } 00086 00087 // Find out current groups mode 00088 $currentgroup = $this->get_current_group($cm, $course, $this->context); 00089 $nostudentsingroup = false; // True if a group is selected and there is no one in it. 00090 if (empty($currentgroup)) { 00091 $currentgroup = 0; 00092 $groupstudents = array(); 00093 00094 } else if ($currentgroup == self::NO_GROUPS_ALLOWED) { 00095 $groupstudents = array(); 00096 $nostudentsingroup = true; 00097 00098 } else { 00099 // All users who can attempt quizzes and who are in the currently selected group 00100 $groupstudents = get_users_by_capability($this->context, 00101 array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'), 00102 '', '', '', '', $currentgroup, '', false); 00103 if (!$groupstudents) { 00104 $nostudentsingroup = true; 00105 } 00106 } 00107 00108 // If recalculate was requested, handle that. 00109 if ($recalculate && confirm_sesskey()) { 00110 $this->clear_cached_data($quiz->id, $currentgroup, $useallattempts); 00111 redirect($reporturl); 00112 } 00113 00114 // Set up the main table. 00115 $this->table = new quiz_report_statistics_table(); 00116 if ($everything) { 00117 $report = get_string('completestatsfilename', 'quiz_statistics'); 00118 } else { 00119 $report = get_string('questionstatsfilename', 'quiz_statistics'); 00120 } 00121 $coursecontext = get_context_instance(CONTEXT_COURSE, $course->id); 00122 $courseshortname = format_string($course->shortname, true, 00123 array('context' => $coursecontext)); 00124 $filename = quiz_report_download_filename($report, $courseshortname, $quiz->name); 00125 $this->table->is_downloading($download, $filename, 00126 get_string('quizstructureanalysis', 'quiz_statistics')); 00127 00128 // Load the questions. 00129 $questions = quiz_report_get_significant_questions($quiz); 00130 $questionids = array(); 00131 foreach ($questions as $question) { 00132 $questionids[] = $question->id; 00133 } 00134 $fullquestions = question_load_questions($questionids); 00135 foreach ($questions as $qno => $question) { 00136 $q = $fullquestions[$question->id]; 00137 $q->maxmark = $question->maxmark; 00138 $q->slot = $qno; 00139 $q->number = $question->number; 00140 $questions[$qno] = $q; 00141 } 00142 00143 // Get the data to be displayed. 00144 list($quizstats, $questions, $subquestions, $s) = 00145 $this->get_quiz_and_questions_stats($quiz, $currentgroup, 00146 $nostudentsingroup, $useallattempts, $groupstudents, $questions); 00147 $quizinfo = $this->get_formatted_quiz_info_data($course, $cm, $quiz, $quizstats); 00148 00149 // Set up the table, if there is data. 00150 if ($s) { 00151 $this->table->setup($quiz, $cm->id, $reporturl, $s); 00152 } 00153 00154 // Print the page header stuff (if not downloading. 00155 if (!$this->table->is_downloading()) { 00156 $this->print_header_and_tabs($cm, $course, $quiz, 'statistics'); 00157 00158 if (groups_get_activity_groupmode($cm)) { 00159 groups_print_activity_menu($cm, $reporturl->out()); 00160 if ($currentgroup && !$groupstudents) { 00161 $OUTPUT->notification(get_string('nostudentsingroup', 'quiz_statistics')); 00162 } 00163 } 00164 00165 if (!quiz_questions_in_quiz($quiz->questions)) { 00166 echo quiz_no_questions_message($quiz, $cm, $this->context); 00167 } else if (!$this->table->is_downloading() && $s == 0) { 00168 echo $OUTPUT->notification(get_string('noattempts', 'quiz')); 00169 } 00170 00171 // Print display options form. 00172 $mform->set_data(array('useallattempts' => $useallattempts)); 00173 $mform->display(); 00174 } 00175 00176 if ($everything) { // Implies is downloading. 00177 // Overall report, then the analysis of each question. 00178 $this->download_quiz_info_table($quizinfo); 00179 00180 if ($s) { 00181 $this->output_quiz_structure_analysis_table($s, $questions, $subquestions); 00182 00183 if ($this->table->is_downloading() == 'xhtml') { 00184 $this->output_statistics_graph($quizstats->id, $s); 00185 } 00186 00187 foreach ($questions as $question) { 00188 if (question_bank::get_qtype( 00189 $question->qtype, false)->can_analyse_responses()) { 00190 $this->output_individual_question_response_analysis( 00191 $question, $reporturl, $quizstats); 00192 00193 } else if (!empty($question->_stats->subquestions)) { 00194 $subitemstodisplay = explode(',', $question->_stats->subquestions); 00195 foreach ($subitemstodisplay as $subitemid) { 00196 $this->output_individual_question_response_analysis( 00197 $subquestions[$subitemid], $reporturl, $quizstats); 00198 } 00199 } 00200 } 00201 } 00202 00203 $this->table->export_class_instance()->finish_document(); 00204 00205 } else if ($slot) { 00206 // Report on an individual question indexed by position. 00207 if (!isset($questions[$slot])) { 00208 print_error('questiondoesnotexist', 'question'); 00209 } 00210 00211 $this->output_individual_question_data($quiz, $questions[$slot]); 00212 $this->output_individual_question_response_analysis( 00213 $questions[$slot], $reporturl, $quizstats); 00214 00215 // Back to overview link. 00216 echo $OUTPUT->box('<a href="' . $reporturl->out() . '">' . 00217 get_string('backtoquizreport', 'quiz_statistics') . '</a>', 00218 'backtomainstats boxaligncenter generalbox boxwidthnormal mdl-align'); 00219 00220 } else if ($qid) { 00221 // Report on an individual sub-question indexed questionid. 00222 if (!isset($subquestions[$qid])) { 00223 print_error('questiondoesnotexist', 'question'); 00224 } 00225 00226 $this->output_individual_question_data($quiz, $subquestions[$qid]); 00227 $this->output_individual_question_response_analysis( 00228 $subquestions[$qid], $reporturl, $quizstats); 00229 00230 // Back to overview link. 00231 echo $OUTPUT->box('<a href="' . $reporturl->out() . '">' . 00232 get_string('backtoquizreport', 'quiz_statistics') . '</a>', 00233 'boxaligncenter generalbox boxwidthnormal mdl-align'); 00234 00235 } else if ($this->table->is_downloading()) { 00236 // Downloading overview report. 00237 $this->download_quiz_info_table($quizinfo); 00238 $this->output_quiz_structure_analysis_table($s, $questions, $subquestions); 00239 $this->table->finish_output(); 00240 00241 } else { 00242 // On-screen display of overview report. 00243 echo $OUTPUT->heading(get_string('quizinformation', 'quiz_statistics')); 00244 echo $this->output_caching_info($quizstats, $quiz->id, $currentgroup, 00245 $groupstudents, $useallattempts, $reporturl); 00246 echo $this->everything_download_options(); 00247 echo $this->output_quiz_info_table($quizinfo); 00248 if ($s) { 00249 echo $OUTPUT->heading(get_string('quizstructureanalysis', 'quiz_statistics')); 00250 $this->output_quiz_structure_analysis_table($s, $questions, $subquestions); 00251 $this->output_statistics_graph($quizstats->id, $s); 00252 } 00253 } 00254 00255 return true; 00256 } 00257 00266 protected function output_individual_question_data($quiz, $question) { 00267 global $OUTPUT; 00268 00269 // On-screen display. Show a summary of the question's place in the quiz, 00270 // and the question statistics. 00271 $datumfromtable = $this->table->format_row($question); 00272 00273 // Set up the question info table. 00274 $questioninfotable = new html_table(); 00275 $questioninfotable->align = array('center', 'center'); 00276 $questioninfotable->width = '60%'; 00277 $questioninfotable->attributes['class'] = 'generaltable titlesleft'; 00278 00279 $questioninfotable->data = array(); 00280 $questioninfotable->data[] = array(get_string('modulename', 'quiz'), $quiz->name); 00281 $questioninfotable->data[] = array(get_string('questionname', 'quiz_statistics'), 00282 $question->name.' '.$datumfromtable['actions']); 00283 $questioninfotable->data[] = array(get_string('questiontype', 'quiz_statistics'), 00284 $datumfromtable['icon'] . ' ' . 00285 question_bank::get_qtype($question->qtype, false)->menu_name() . ' ' . 00286 $datumfromtable['icon']); 00287 $questioninfotable->data[] = array(get_string('positions', 'quiz_statistics'), 00288 $question->_stats->positions); 00289 00290 // Set up the question statistics table. 00291 $questionstatstable = new html_table(); 00292 $questionstatstable->align = array('center', 'center'); 00293 $questionstatstable->width = '60%'; 00294 $questionstatstable->attributes['class'] = 'generaltable titlesleft'; 00295 00296 unset($datumfromtable['number']); 00297 unset($datumfromtable['icon']); 00298 $actions = $datumfromtable['actions']; 00299 unset($datumfromtable['actions']); 00300 unset($datumfromtable['name']); 00301 $labels = array( 00302 's' => get_string('attempts', 'quiz_statistics'), 00303 'facility' => get_string('facility', 'quiz_statistics'), 00304 'sd' => get_string('standarddeviationq', 'quiz_statistics'), 00305 'random_guess_score' => get_string('random_guess_score', 'quiz_statistics'), 00306 'intended_weight' => get_string('intended_weight', 'quiz_statistics'), 00307 'effective_weight' => get_string('effective_weight', 'quiz_statistics'), 00308 'discrimination_index' => get_string('discrimination_index', 'quiz_statistics'), 00309 'discriminative_efficiency' => 00310 get_string('discriminative_efficiency', 'quiz_statistics') 00311 ); 00312 foreach ($datumfromtable as $item => $value) { 00313 $questionstatstable->data[] = array($labels[$item], $value); 00314 } 00315 00316 // Display the various bits. 00317 echo $OUTPUT->heading(get_string('questioninformation', 'quiz_statistics')); 00318 echo html_writer::table($questioninfotable); 00319 echo $this->render_question_text($question); 00320 echo $OUTPUT->heading(get_string('questionstatistics', 'quiz_statistics')); 00321 echo html_writer::table($questionstatstable); 00322 } 00323 public function format_text($text, $format, $qa, $component, $filearea, $itemid, 00324 $clean = false) { 00325 $formatoptions = new stdClass(); 00326 $formatoptions->noclean = !$clean; 00327 $formatoptions->para = false; 00328 $text = $qa->rewrite_pluginfile_urls($text, $component, $filearea, $itemid); 00329 return format_text($text, $format, $formatoptions); 00330 } 00331 00333 public function format_questiontext($qa) { 00334 return $this->format_text($this->questiontext, $this->questiontextformat, 00335 $qa, 'question', 'questiontext', $this->id); 00336 } 00337 00342 protected function render_question_text($question) { 00343 global $OUTPUT; 00344 00345 $text = question_rewrite_questiontext_preview_urls($question->questiontext, 00346 $this->context->id, 'quiz_statistics', $question->id); 00347 00348 return $OUTPUT->box(format_text($text, $question->questiontextformat, 00349 array('noclean' => true, 'para' => false, 'overflowdiv' => true)), 00350 'questiontext boxaligncenter generalbox boxwidthnormal mdl-align'); 00351 } 00352 00359 protected function output_individual_question_response_analysis($question, 00360 $reporturl, $quizstats) { 00361 global $OUTPUT; 00362 00363 if (!question_bank::get_qtype($question->qtype, false)->can_analyse_responses()) { 00364 return; 00365 } 00366 00367 $qtable = new quiz_report_statistics_question_table($question->id); 00368 $exportclass = $this->table->export_class_instance(); 00369 $qtable->export_class_instance($exportclass); 00370 if (!$this->table->is_downloading()) { 00371 // Output an appropriate title. 00372 echo $OUTPUT->heading(get_string('analysisofresponses', 'quiz_statistics')); 00373 00374 } else { 00375 // Work out an appropriate title. 00376 $questiontabletitle = '"' . $question->name . '"'; 00377 if (!empty($question->number)) { 00378 $questiontabletitle = '(' . $question->number . ') ' . $questiontabletitle; 00379 } 00380 if ($this->table->is_downloading() == 'xhtml') { 00381 $questiontabletitle = get_string('analysisofresponsesfor', 00382 'quiz_statistics', $questiontabletitle); 00383 } 00384 00385 // Set up the table. 00386 $exportclass->start_table($questiontabletitle); 00387 00388 if ($this->table->is_downloading() == 'xhtml') { 00389 echo $this->render_question_text($question); 00390 } 00391 } 00392 00393 $responesstats = new quiz_statistics_response_analyser($question); 00394 $responesstats->load_cached($quizstats->id); 00395 00396 $qtable->setup($reporturl, $question, $responesstats); 00397 if ($this->table->is_downloading()) { 00398 $exportclass->output_headers($qtable->headers); 00399 } 00400 00401 foreach ($responesstats->responseclasses as $partid => $partclasses) { 00402 $rowdata = new stdClass(); 00403 $rowdata->part = $partid; 00404 foreach ($partclasses as $responseclassid => $responseclass) { 00405 $rowdata->responseclass = $responseclass->responseclass; 00406 00407 $responsesdata = $responesstats->responses[$partid][$responseclassid]; 00408 if (empty($responsesdata)) { 00409 if (!array_key_exists('responseclass', $qtable->columns)) { 00410 $rowdata->response = $responseclass->responseclass; 00411 } else { 00412 $rowdata->response = ''; 00413 } 00414 $rowdata->fraction = $responseclass->fraction; 00415 $rowdata->count = 0; 00416 $qtable->add_data_keyed($qtable->format_row($rowdata)); 00417 continue; 00418 } 00419 00420 foreach ($responsesdata as $response => $data) { 00421 $rowdata->response = $response; 00422 $rowdata->fraction = $data->fraction; 00423 $rowdata->count = $data->count; 00424 $qtable->add_data_keyed($qtable->format_row($rowdata)); 00425 } 00426 } 00427 } 00428 00429 $qtable->finish_output(!$this->table->is_downloading()); 00430 } 00431 00438 protected function output_quiz_structure_analysis_table($s, $questions, $subquestions) { 00439 if (!$s) { 00440 return; 00441 } 00442 00443 foreach ($questions as $question) { 00444 // Output the data for this questions. 00445 $this->table->add_data_keyed($this->table->format_row($question)); 00446 00447 if (empty($question->_stats->subquestions)) { 00448 continue; 00449 } 00450 00451 // And its subquestions, if it has any. 00452 $subitemstodisplay = explode(',', $question->_stats->subquestions); 00453 foreach ($subitemstodisplay as $subitemid) { 00454 $subquestions[$subitemid]->maxmark = $question->maxmark; 00455 $this->table->add_data_keyed($this->table->format_row($subquestions[$subitemid])); 00456 } 00457 } 00458 00459 $this->table->finish_output(!$this->table->is_downloading()); 00460 } 00461 00462 protected function get_formatted_quiz_info_data($course, $cm, $quiz, $quizstats) { 00463 00464 // You can edit this array to control which statistics are displayed. 00465 $todisplay = array('firstattemptscount' => 'number', 00466 'allattemptscount' => 'number', 00467 'firstattemptsavg' => 'summarks_as_percentage', 00468 'allattemptsavg' => 'summarks_as_percentage', 00469 'median' => 'summarks_as_percentage', 00470 'standarddeviation' => 'summarks_as_percentage', 00471 'skewness' => 'number_format', 00472 'kurtosis' => 'number_format', 00473 'cic' => 'number_format_percent', 00474 'errorratio' => 'number_format_percent', 00475 'standarderror' => 'summarks_as_percentage'); 00476 00477 // General information about the quiz. 00478 $quizinfo = array(); 00479 $quizinfo[get_string('quizname', 'quiz_statistics')] = format_string($quiz->name); 00480 $quizinfo[get_string('coursename', 'quiz_statistics')] = format_string($course->fullname); 00481 if ($cm->idnumber) { 00482 $quizinfo[get_string('idnumbermod')] = $cm->idnumber; 00483 } 00484 if ($quiz->timeopen) { 00485 $quizinfo[get_string('quizopen', 'quiz')] = userdate($quiz->timeopen); 00486 } 00487 if ($quiz->timeclose) { 00488 $quizinfo[get_string('quizclose', 'quiz')] = userdate($quiz->timeclose); 00489 } 00490 if ($quiz->timeopen && $quiz->timeclose) { 00491 $quizinfo[get_string('duration', 'quiz_statistics')] = 00492 format_time($quiz->timeclose - $quiz->timeopen); 00493 } 00494 00495 // The statistics. 00496 foreach ($todisplay as $property => $format) { 00497 if (!isset($quizstats->$property) || empty($format[$property])) { 00498 continue; 00499 } 00500 $value = $quizstats->$property; 00501 00502 switch ($format) { 00503 case 'summarks_as_percentage': 00504 $formattedvalue = quiz_report_scale_summarks_as_percentage($value, $quiz); 00505 break; 00506 case 'number_format_percent': 00507 $formattedvalue = quiz_format_grade($quiz, $value) . '%'; 00508 break; 00509 case 'number_format': 00510 // + 2 decimal places, since not a percentage, 00511 // and we want the same number of sig figs. 00512 $formattedvalue = format_float($value, $quiz->decimalpoints + 2); 00513 break; 00514 case 'number': 00515 $formattedvalue = $value + 0; 00516 break; 00517 default: 00518 $formattedvalue = $value; 00519 } 00520 00521 $quizinfo[get_string($property, 'quiz_statistics', 00522 $this->using_attempts_string(!empty($quizstats->allattempts)))] = 00523 $formattedvalue; 00524 } 00525 00526 return $quizinfo; 00527 } 00528 00534 protected function output_quiz_info_table($quizinfo) { 00535 00536 $quizinfotable = new html_table(); 00537 $quizinfotable->align = array('center', 'center'); 00538 $quizinfotable->width = '60%'; 00539 $quizinfotable->attributes['class'] = 'generaltable titlesleft'; 00540 $quizinfotable->data = array(); 00541 00542 foreach ($quizinfo as $heading => $value) { 00543 $quizinfotable->data[] = array($heading, $value); 00544 } 00545 00546 return html_writer::table($quizinfotable); 00547 } 00548 00553 protected function download_quiz_info_table($quizinfo) { 00554 global $OUTPUT; 00555 00556 // XHTML download is a special case. 00557 if ($this->table->is_downloading() == 'xhtml') { 00558 echo $OUTPUT->heading(get_string('quizinformation', 'quiz_statistics')); 00559 echo $this->output_quiz_info_table($quizinfo); 00560 return; 00561 } 00562 00563 // Reformat the data ready for output. 00564 $headers = array(); 00565 $row = array(); 00566 foreach ($quizinfo as $heading => $value) { 00567 $headers[] = $heading; 00568 $row[] = $value; 00569 } 00570 00571 // Do the output. 00572 $exportclass = $this->table->export_class_instance(); 00573 $exportclass->start_table(get_string('quizinformation', 'quiz_statistics')); 00574 $exportclass->output_headers($headers); 00575 $exportclass->add_data($row); 00576 $exportclass->finish_table(); 00577 } 00578 00583 protected function output_statistics_graph($quizstatsid, $s) { 00584 global $OUTPUT; 00585 00586 if ($s == 0) { 00587 return; 00588 } 00589 00590 $imageurl = new moodle_url('/mod/quiz/report/statistics/statistics_graph.php', 00591 array('id' => $quizstatsid)); 00592 $OUTPUT->heading(get_string('statisticsreportgraph', 'quiz_statistics')); 00593 echo html_writer::tag('div', html_writer::empty_tag('img', array('src' => $imageurl, 00594 'alt' => get_string('statisticsreportgraph', 'quiz_statistics'))), 00595 array('class' => 'graph')); 00596 } 00597 00609 protected function get_emtpy_stats($questions, $firstattemptscount = 0, 00610 $allattemptscount = 0) { 00611 $quizstats = new stdClass(); 00612 $quizstats->firstattemptscount = $firstattemptscount; 00613 $quizstats->allattemptscount = $allattemptscount; 00614 00615 $qstats = new stdClass(); 00616 $qstats->questions = $questions; 00617 $qstats->subquestions = array(); 00618 $qstats->responses = array(); 00619 00620 return array(0, $quizstats, false); 00621 } 00622 00637 protected function compute_stats($quizid, $currentgroup, $nostudentsingroup, 00638 $useallattempts, $groupstudents, $questions) { 00639 global $DB; 00640 00641 // Calculating MEAN of marks for all attempts by students 00642 // http://docs.moodle.org/dev/Quiz_item_analysis_calculations_in_practise 00643 // #Calculating_MEAN_of_grades_for_all_attempts_by_students 00644 if ($nostudentsingroup) { 00645 return $this->get_emtpy_stats($questions); 00646 } 00647 00648 list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql( 00649 $quizid, $currentgroup, $groupstudents, true); 00650 00651 $attempttotals = $DB->get_records_sql(" 00652 SELECT 00653 CASE WHEN attempt = 1 THEN 1 ELSE 0 END AS isfirst, 00654 COUNT(1) AS countrecs, 00655 SUM(sumgrades) AS total 00656 FROM $fromqa 00657 WHERE $whereqa 00658 GROUP BY CASE WHEN attempt = 1 THEN 1 ELSE 0 END", $qaparams); 00659 00660 if (!$attempttotals) { 00661 return $this->get_emtpy_stats($questions); 00662 } 00663 00664 if (isset($attempttotals[1])) { 00665 $firstattempts = $attempttotals[1]; 00666 $firstattempts->average = $firstattempts->total / $firstattempts->countrecs; 00667 } else { 00668 $firstattempts = new stdClass(); 00669 $firstattempts->countrecs = 0; 00670 $firstattempts->total = 0; 00671 $firstattempts->average = null; 00672 } 00673 00674 $allattempts = new stdClass(); 00675 if (isset($attempttotals[0])) { 00676 $allattempts->countrecs = $firstattempts->countrecs + $attempttotals[0]->countrecs; 00677 $allattempts->total = $firstattempts->total + $attempttotals[0]->total; 00678 } else { 00679 $allattempts->countrecs = $firstattempts->countrecs; 00680 $allattempts->total = $firstattempts->total; 00681 } 00682 00683 if ($useallattempts) { 00684 $usingattempts = $allattempts; 00685 $usingattempts->sql = ''; 00686 } else { 00687 $usingattempts = $firstattempts; 00688 $usingattempts->sql = 'AND quiza.attempt = 1 '; 00689 } 00690 00691 $s = $usingattempts->countrecs; 00692 if ($s == 0) { 00693 return $this->get_emtpy_stats($questions, $firstattempts->countrecs, 00694 $allattempts->countrecs); 00695 } 00696 $summarksavg = $usingattempts->total / $usingattempts->countrecs; 00697 00698 $quizstats = new stdClass(); 00699 $quizstats->allattempts = $useallattempts; 00700 $quizstats->firstattemptscount = $firstattempts->countrecs; 00701 $quizstats->allattemptscount = $allattempts->countrecs; 00702 $quizstats->firstattemptsavg = $firstattempts->average; 00703 $quizstats->allattemptsavg = $allattempts->total / $allattempts->countrecs; 00704 00705 // Recalculate sql again this time possibly including test for first attempt. 00706 list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql( 00707 $quizid, $currentgroup, $groupstudents, $useallattempts); 00708 00709 // Median 00710 if ($s % 2 == 0) { 00711 //even number of attempts 00712 $limitoffset = $s/2 - 1; 00713 $limit = 2; 00714 } else { 00715 $limitoffset = floor($s/2); 00716 $limit = 1; 00717 } 00718 $sql = "SELECT id, sumgrades 00719 FROM $fromqa 00720 WHERE $whereqa 00721 ORDER BY sumgrades"; 00722 00723 $medianmarks = $DB->get_records_sql_menu($sql, $qaparams, $limitoffset, $limit); 00724 00725 $quizstats->median = array_sum($medianmarks) / count($medianmarks); 00726 if ($s > 1) { 00727 //fetch sum of squared, cubed and power 4d 00728 //differences between marks and mean mark 00729 $mean = $usingattempts->total / $s; 00730 $sql = "SELECT 00731 SUM(POWER((quiza.sumgrades - $mean), 2)) AS power2, 00732 SUM(POWER((quiza.sumgrades - $mean), 3)) AS power3, 00733 SUM(POWER((quiza.sumgrades - $mean), 4)) AS power4 00734 FROM $fromqa 00735 WHERE $whereqa"; 00736 $params = array('mean1' => $mean, 'mean2' => $mean, 'mean3' => $mean)+$qaparams; 00737 00738 $powers = $DB->get_record_sql($sql, $params, MUST_EXIST); 00739 00740 // Standard_Deviation 00741 // see http://docs.moodle.org/dev/Quiz_item_analysis_calculations_in_practise 00742 // #Standard_Deviation 00743 00744 $quizstats->standarddeviation = sqrt($powers->power2 / ($s - 1)); 00745 00746 // Skewness 00747 if ($s > 2) { 00748 // see http://docs.moodle.org/dev/ 00749 // Quiz_item_analysis_calculations_in_practise#Skewness_and_Kurtosis 00750 $m2= $powers->power2 / $s; 00751 $m3= $powers->power3 / $s; 00752 $m4= $powers->power4 / $s; 00753 00754 $k2= $s*$m2/($s-1); 00755 $k3= $s*$s*$m3/(($s-1)*($s-2)); 00756 if ($k2) { 00757 $quizstats->skewness = $k3 / (pow($k2, 3/2)); 00758 } 00759 } 00760 00761 // Kurtosis 00762 if ($s > 3) { 00763 $k4= $s*$s*((($s+1)*$m4)-(3*($s-1)*$m2*$m2))/(($s-1)*($s-2)*($s-3)); 00764 if ($k2) { 00765 $quizstats->kurtosis = $k4 / ($k2*$k2); 00766 } 00767 } 00768 } 00769 00770 $qstats = new quiz_statistics_question_stats($questions, $s, $summarksavg); 00771 $qstats->load_step_data($quizid, $currentgroup, $groupstudents, $useallattempts); 00772 $qstats->compute_statistics(); 00773 00774 if ($s > 1) { 00775 $p = count($qstats->questions); // No of positions 00776 if ($p > 1 && isset($k2)) { 00777 $quizstats->cic = (100 * $p / ($p -1)) * 00778 (1 - ($qstats->get_sum_of_mark_variance()) / $k2); 00779 $quizstats->errorratio = 100 * sqrt(1 - ($quizstats->cic / 100)); 00780 $quizstats->standarderror = $quizstats->errorratio * 00781 $quizstats->standarddeviation / 100; 00782 } 00783 } 00784 00785 return array($s, $quizstats, $qstats); 00786 } 00787 00804 protected function try_loading_cached_stats($quiz, $currentgroup, 00805 $nostudentsingroup, $useallattempts, $groupstudents, $questions) { 00806 global $DB; 00807 00808 $timemodified = time() - self::TIME_TO_CACHE_STATS; 00809 $quizstats = $DB->get_record_select('quiz_statistics', 00810 'quizid = ? AND groupid = ? AND allattempts = ? AND timemodified > ?', 00811 array($quiz->id, $currentgroup, $useallattempts, $timemodified)); 00812 00813 if (!$quizstats) { 00814 // No cached data found. 00815 return array(null, $questions, null, null); 00816 } 00817 00818 if ($useallattempts) { 00819 $s = $quizstats->allattemptscount; 00820 } else { 00821 $s = $quizstats->firstattemptscount; 00822 } 00823 00824 $subquestions = array(); 00825 $questionstats = $DB->get_records('quiz_question_statistics', 00826 array('quizstatisticsid' => $quizstats->id)); 00827 00828 $subquestionstats = array(); 00829 foreach ($questionstats as $stat) { 00830 if ($stat->slot) { 00831 $questions[$stat->slot]->_stats = $stat; 00832 } else { 00833 $subquestionstats[$stat->questionid] = $stat; 00834 } 00835 } 00836 00837 if (!empty($subquestionstats)) { 00838 $subqstofetch = array_keys($subquestionstats); 00839 $subquestions = question_load_questions($subqstofetch); 00840 foreach ($subquestions as $subqid => $subq) { 00841 $subquestions[$subqid]->_stats = $subquestionstats[$subqid]; 00842 $subquestions[$subqid]->maxmark = $subq->defaultmark; 00843 } 00844 } 00845 00846 return array($quizstats, $questions, $subquestions, $s); 00847 } 00848 00859 protected function cache_stats($quizid, $currentgroup, 00860 $quizstats, $questions, $subquestions) { 00861 global $DB; 00862 00863 $toinsert = clone($quizstats); 00864 $toinsert->quizid = $quizid; 00865 $toinsert->groupid = $currentgroup; 00866 $toinsert->timemodified = time(); 00867 00868 // Fix up some dodgy data. 00869 if (isset($toinsert->errorratio) && is_nan($toinsert->errorratio)) { 00870 $toinsert->errorratio = null; 00871 } 00872 if (isset($toinsert->standarderror) && is_nan($toinsert->standarderror)) { 00873 $toinsert->standarderror = null; 00874 } 00875 00876 // Store the data. 00877 $quizstats->id = $DB->insert_record('quiz_statistics', $toinsert); 00878 00879 foreach ($questions as $question) { 00880 $question->_stats->quizstatisticsid = $quizstats->id; 00881 $DB->insert_record('quiz_question_statistics', $question->_stats, false); 00882 } 00883 00884 foreach ($subquestions as $subquestion) { 00885 $subquestion->_stats->quizstatisticsid = $quizstats->id; 00886 $DB->insert_record('quiz_question_statistics', $subquestion->_stats, false); 00887 } 00888 00889 return $quizstats->id; 00890 } 00891 00908 protected function get_quiz_and_questions_stats($quiz, $currentgroup, 00909 $nostudentsingroup, $useallattempts, $groupstudents, $questions) { 00910 00911 list($quizstats, $questions, $subquestions, $s) = 00912 $this->try_loading_cached_stats($quiz, $currentgroup, $nostudentsingroup, 00913 $useallattempts, $groupstudents, $questions); 00914 00915 if (is_null($quizstats)) { 00916 list($s, $quizstats, $qstats) = $this->compute_stats($quiz->id, 00917 $currentgroup, $nostudentsingroup, $useallattempts, $groupstudents, $questions); 00918 00919 if ($s) { 00920 $questions = $qstats->questions; 00921 $subquestions = $qstats->subquestions; 00922 00923 $quizstatisticsid = $this->cache_stats($quiz->id, $currentgroup, 00924 $quizstats, $questions, $subquestions); 00925 00926 $this->analyse_responses($quizstatisticsid, $quiz->id, $currentgroup, 00927 $nostudentsingroup, $useallattempts, $groupstudents, 00928 $questions, $subquestions); 00929 } 00930 } 00931 00932 return array($quizstats, $questions, $subquestions, $s); 00933 } 00934 00935 protected function analyse_responses($quizstatisticsid, $quizid, $currentgroup, 00936 $nostudentsingroup, $useallattempts, $groupstudents, $questions, $subquestions) { 00937 00938 $qubaids = quiz_statistics_qubaids_condition( 00939 $quizid, $currentgroup, $groupstudents, $useallattempts); 00940 00941 $done = array(); 00942 foreach ($questions as $question) { 00943 if (!question_bank::get_qtype($question->qtype, false)->can_analyse_responses()) { 00944 continue; 00945 } 00946 $done[$question->id] = 1; 00947 00948 $responesstats = new quiz_statistics_response_analyser($question); 00949 $responesstats->analyse($qubaids); 00950 $responesstats->store_cached($quizstatisticsid); 00951 } 00952 00953 foreach ($subquestions as $question) { 00954 if (!question_bank::get_qtype($question->qtype, false)->can_analyse_responses() || 00955 isset($done[$question->id])) { 00956 continue; 00957 } 00958 $done[$question->id] = 1; 00959 00960 $responesstats = new quiz_statistics_response_analyser($question); 00961 $responesstats->analyse($qubaids); 00962 $responesstats->store_cached($quizstatisticsid); 00963 } 00964 } 00965 00969 protected function everything_download_options() { 00970 $downloadoptions = $this->table->get_download_menu(); 00971 00972 $output = '<form action="'. $this->table->baseurl .'" method="post">'; 00973 $output .= '<div class="mdl-align">'; 00974 $output .= '<input type="hidden" name="everything" value="1"/>'; 00975 $output .= '<input type="submit" value="' . 00976 get_string('downloadeverything', 'quiz_statistics') . '"/>'; 00977 $output .= html_writer::select($downloadoptions, 'download', 00978 $this->table->defaultdownloadformat, false); 00979 $output .= '</div></form>'; 00980 00981 return $output; 00982 } 00983 00996 protected function output_caching_info($quizstats, $quizid, $currentgroup, 00997 $groupstudents, $useallattempts, $reporturl) { 00998 global $DB, $OUTPUT; 00999 01000 if (empty($quizstats->timemodified)) { 01001 return ''; 01002 } 01003 01004 // Find the number of attempts since the cached statistics were computed. 01005 list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql( 01006 $quizid, $currentgroup, $groupstudents, $useallattempts, true); 01007 $count = $DB->count_records_sql(" 01008 SELECT COUNT(1) 01009 FROM $fromqa 01010 WHERE $whereqa 01011 AND quiza.timefinish > {$quizstats->timemodified}", $qaparams); 01012 01013 if (!$count) { 01014 $count = 0; 01015 } 01016 01017 // Generate the output. 01018 $a = new stdClass(); 01019 $a->lastcalculated = format_time(time() - $quizstats->timemodified); 01020 $a->count = $count; 01021 01022 $recalcualteurl = new moodle_url($reporturl, 01023 array('recalculate' => 1, 'sesskey' => sesskey())); 01024 $output = ''; 01025 $output .= $OUTPUT->box_start( 01026 'boxaligncenter generalbox boxwidthnormal mdl-align', 'cachingnotice'); 01027 $output .= get_string('lastcalculated', 'quiz_statistics', $a); 01028 $output .= $OUTPUT->single_button($recalcualteurl, 01029 get_string('recalculatenow', 'quiz_statistics')); 01030 $output .= $OUTPUT->box_end(true); 01031 01032 return $output; 01033 } 01034 01042 protected function clear_cached_data($quizid, $currentgroup, $useallattempts) { 01043 global $DB; 01044 01045 $todelete = $DB->get_records_menu('quiz_statistics', array('quizid' => $quizid, 01046 'groupid' => $currentgroup, 'allattempts' => $useallattempts), '', 'id, 1'); 01047 01048 if (!$todelete) { 01049 return; 01050 } 01051 01052 list($todeletesql, $todeleteparams) = $DB->get_in_or_equal(array_keys($todelete)); 01053 01054 $DB->delete_records_select('quiz_question_statistics', 01055 'quizstatisticsid ' . $todeletesql, $todeleteparams); 01056 $DB->delete_records_select('quiz_question_response_stats', 01057 'quizstatisticsid ' . $todeletesql, $todeleteparams); 01058 $DB->delete_records_select('quiz_statistics', 01059 'id ' . $todeletesql, $todeleteparams); 01060 } 01061 01066 protected function using_attempts_string($useallattempts) { 01067 if ($useallattempts) { 01068 return get_string('allattempts', 'quiz_statistics'); 01069 } else { 01070 return get_string('firstattempts', 'quiz_statistics'); 01071 } 01072 } 01073 } 01074 01075 function quiz_statistics_attempts_sql($quizid, $currentgroup, $groupstudents, 01076 $allattempts = true, $includeungraded = false) { 01077 global $DB; 01078 01079 $fromqa = '{quiz_attempts} quiza '; 01080 01081 $whereqa = 'quiza.quiz = :quizid AND quiza.preview = 0 AND quiza.timefinish <> 0'; 01082 $qaparams = array('quizid' => $quizid); 01083 01084 if (!empty($currentgroup) && $groupstudents) { 01085 list($grpsql, $grpparams) = $DB->get_in_or_equal(array_keys($groupstudents), 01086 SQL_PARAMS_NAMED, 'u'); 01087 $whereqa .= " AND quiza.userid $grpsql"; 01088 $qaparams += $grpparams; 01089 } 01090 01091 if (!$allattempts) { 01092 $whereqa .= ' AND quiza.attempt = 1'; 01093 } 01094 01095 if (!$includeungraded) { 01096 $whereqa .= ' AND quiza.sumgrades IS NOT NULL'; 01097 } 01098 01099 return array($fromqa, $whereqa, $qaparams); 01100 } 01101 01108 function quiz_statistics_qubaids_condition($quizid, $currentgroup, $groupstudents, 01109 $allattempts = true, $includeungraded = false) { 01110 list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql($quizid, $currentgroup, 01111 $groupstudents, $allattempts, $includeungraded); 01112 return new qubaid_join($fromqa, 'quiza.uniqueid', $whereqa, $qaparams); 01113 }