Moodle  2.2.1
http://www.collinsharper.com
C:/xampp/htdocs/moodle/mod/quiz/report/statistics/report.php
Go to the documentation of this file.
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.'&nbsp;'.$datumfromtable['actions']);
00283         $questioninfotable->data[] = array(get_string('questiontype', 'quiz_statistics'),
00284                 $datumfromtable['icon'] . '&nbsp;' .
00285                 question_bank::get_qtype($question->qtype, false)->menu_name() . '&nbsp;' .
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 }
 All Data Structures Namespaces Files Functions Variables Enumerations