MDL-19976 Changing redirect($url->out()) to redirect($url). NEVER, NEVER use redirect...
[moodle.git] / mod / quiz / report / statistics / report.php
1 <?php
2 /**
3  * This script calculates various statistics about student attempts
4  *
5  * @version $Id$
6  * @author Martin Dougiamas, Jamie Pratt, Tim Hunt and others.
7  * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
8  * @package quiz
9  **/
11 define('QUIZ_REPORT_TIME_TO_CACHE_STATS', MINSECS * 15);
12 require_once($CFG->dirroot.'/mod/quiz/report/statistics/statistics_form.php');
13 require_once($CFG->dirroot.'/mod/quiz/report/statistics/statistics_table.php');
15 class quiz_statistics_report extends quiz_default_report {
16     
17     /**
18      * @var object instance of table class used for main questions stats table.
19      */
20     var $table;
22     /**
23      * Display the report.
24      */
25     function display($quiz, $cm, $course) {
26         global $CFG, $DB, $QTYPES;
28         $context = get_context_instance(CONTEXT_MODULE, $cm->id);
30         $download = optional_param('download', '', PARAM_ALPHA);
31         $everything = optional_param('everything', 0, PARAM_BOOL);
32         $recalculate = optional_param('recalculate', 0, PARAM_BOOL);
33         //pass the question id for detailed analysis question
34         $qid = optional_param('qid', 0, PARAM_INT);
35         $pageoptions = array();
36         $pageoptions['id'] = $cm->id;
37         $pageoptions['q'] = $quiz->id;
38         $pageoptions['mode'] = 'statistics';
39         
40         $questions = quiz_report_load_questions($quiz);
41         // Load the question type specific information
42         if (!get_question_options($questions)) {
43             print_error('cannotloadquestion', 'question');
44         }
46         
47         $reporturl = new moodle_url($CFG->wwwroot.'/mod/quiz/report.php', $pageoptions);
49         $mform = new mod_quiz_report_statistics($reporturl);
50         if ($fromform = $mform->get_data()){
51             $useallattempts = $fromform->useallattempts;
52             if ($fromform->useallattempts){
53                 set_user_preference('quiz_report_statistics_useallattempts', $fromform->useallattempts);
54             } else {
55                 unset_user_preference('quiz_report_statistics_useallattempts');
56             }
57         } else {
58             $useallattempts = get_user_preferences('quiz_report_statistics_useallattempts', 0);
59         }
61         /// find out current groups mode
62         $currentgroup = groups_get_activity_group($cm, true);
63         
65         $nostudentsingroup = false;//true if a group is selected and their is noeone in it.
66         if (!empty($currentgroup)) {
67             // all users who can attempt quizzes and who are in the currently selected group
68             $groupstudents = get_users_by_capability($context, array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'),'','','','',$currentgroup,'',false);
69             if (!$groupstudents){
70                 $nostudentsingroup = true;
71             }
72         } else {
73             $groupstudents = array();
74         }
75         
76         if ($recalculate){
77             if ($todelete = $DB->get_records_menu('quiz_statistics', array('quizid' => $quiz->id, 'groupid'=> (int)$currentgroup, 'allattempts'=>$useallattempts))){
78                 list($todeletesql, $todeleteparams) = $DB->get_in_or_equal(array_keys($todelete));
79                 if (!$DB->delete_records_select('quiz_statistics', "id $todeletesql", $todeleteparams)){
80                     print_error('errordeleting', 'quiz_statistics', '', 'quiz_statistics');
81                 }
82                 if (!$DB->delete_records_select('quiz_question_statistics', "quizstatisticsid $todeletesql", $todeleteparams)){
83                     print_error('errordeleting', 'quiz_statistics', '', 'quiz_question_statistics');
84                 }
85                 if (!$DB->delete_records_select('quiz_question_response_stats', "quizstatisticsid $todeletesql", $todeleteparams)){
86                     print_error('errordeleting', 'quiz_statistics', '', 'quiz_question_response_stats');
87                 }
88             }
89             redirect($reporturl);
90         }
91         
92         
93         $this->table = new quiz_report_statistics_table();
94         $filename = "$course->shortname-".format_string($quiz->name,true);
95         $this->table->is_downloading($download, $filename, get_string('quizstructureanalysis', 'quiz_statistics'));
96         if (!$this->table->is_downloading()) {
97             // Only print headers if not asked to download data
98             $this->print_header_and_tabs($cm, $course, $quiz, "statistics");
99         }
101         if ($groupmode = groups_get_activity_groupmode($cm)) {   // Groups are being used
102             if (!$this->table->is_downloading()) {
103                 groups_print_activity_menu($cm, $reporturl->out());
104                 echo '<br />';
105                 if ($currentgroup && !$groupstudents){
106                     notify(get_string('nostudentsingroup', 'quiz_statistics'));
107                 }
108             }
109         }
111         if (!$this->table->is_downloading()) {
112             // Print display options
113             $mform->set_data(array('useallattempts' => $useallattempts));
114             $mform->display();
115         }
117         list($quizstats, $questions, $subquestions, $s, $usingattemptsstring) 
118             = $this->quiz_questions_stats($quiz, $currentgroup, $nostudentsingroup,
119                                         $useallattempts, $groupstudents, $questions);
121         if (!$this->table->is_downloading()){
122             if ($s==0){
123                 print_heading(get_string('noattempts','quiz'));
124             }
125         }
126         if ($s){
127             $this->table->setup($quiz, $cm->id, $reporturl, $s);
128         }
129         
130         if (!$qid){//main page
131             $this->output_quiz_info_table($course, $cm, $quiz, $quizstats, $usingattemptsstring, $currentgroup, $groupstudents, $useallattempts, $download, $reporturl, $everything);
132             $this->output_quiz_structure_analysis_table($s, $questions, $subquestions);
133             if (!$this->table->is_downloading() || ($everything && $this->table->is_downloading() == 'xhtml')){
134                 if ($s > 1){
135                     $imageurl = $CFG->wwwroot.'/mod/quiz/report/statistics/statistics_graph.php?id='.$quizstats->id;
136                     print_heading(get_string('statisticsreportgraph', 'quiz_statistics'));
137                     echo '<div class="mdl-align"><img src="'.$imageurl.'" alt="'.get_string('statisticsreportgraph', 'quiz_statistics').'" /></div>';
138                 }
139             }
140             if ($this->table->is_downloading()){
141                 if ($everything){
142                     foreach ($questions as $question){
143                         if ($question->qtype != 'random' && $QTYPES[$question->qtype]->show_analysis_of_responses()){
144                             $this->output_individual_question_data($quiz, $question, $reporturl, $quizstats);
145                         } elseif (!empty($question->_stats->subquestions)) {
146                             $subitemstodisplay = explode(',', $question->_stats->subquestions);
147                             foreach ($subitemstodisplay as $subitemid){
148                                 $this->output_individual_question_data($quiz, $subquestions[$subitemid], $reporturl, $quizstats);
149                             }
150                         }
151                     }
152                     $exportclassinstance =& $this->table->export_class_instance();
153                 } else {
154                     $this->table->finish_output();
155                 }
156             }
157             if ($this->table->is_downloading() && $everything){
158                 $exportclassinstance->finish_document();
159             }
160         } else {//individual question page
161             $thisquestion = false;
162             if (isset($questions[$qid])){
163                 $thisquestion = $questions[$qid];
164             } else if (isset($subquestions[$qid])){
165                 $thisquestion = $subquestions[$qid];
166             } else {
167                 print_error('questiondoesnotexist', 'question');
168             }
169             $this->output_individual_question_data($quiz, $thisquestion, $reporturl, $quizstats);
170         }
171         return true;
172     }
173     
174     function sort_response_details($detail1, $detail2){
175         if ($detail1->credit == $detail2->credit){
176             return strcmp($detail1->answer, $detail2->answer);
177         }
178         return ($detail1->credit > $detail2->credit) ? -1 : 1;
179     }
180     function sort_answers($answer1, $answer2){
181         if ($answer1->rcount == $answer2->rcount){
182             return strcmp($answer1->response, $answer2->response);
183         } else {
184             return ($answer1->rcount > $answer2->rcount)? -1 : 1;
185         }
186     }
187     
188     function output_individual_question_data($quiz, $question, $reporturl, $quizstats){
189         global $CFG, $DB, $QTYPES;
190         require_once($CFG->dirroot.'/mod/quiz/report/statistics/statistics_question_table.php');
191         $this->qtable = new quiz_report_statistics_question_table($question->id);
192         $downloadtype = $this->table->is_downloading();
193         if (!$this->table->is_downloading()){
194             $datumfromtable = $this->table->format_row($question);
195     
196             $questioninfotable = new object();
197             $questioninfotable->align = array('center', 'center');
198             $questioninfotable->width = '60%';
199             $questioninfotable->class = 'generaltable titlesleft';
200             
201             $questioninfotable->data = array();
202             $questioninfotable->data[] = array(get_string('modulename', 'quiz'), $quiz->name);
203             $questioninfotable->data[] = array(get_string('questionname', 'quiz_statistics'), $question->name.'&nbsp;'.$datumfromtable['actions']);
204             $questioninfotable->data[] = array(get_string('questiontype', 'quiz_statistics'), $datumfromtable['icon'].'&nbsp;'.get_string($question->qtype,'quiz').'&nbsp;'.$datumfromtable['icon']);
205             $questioninfotable->data[] = array(get_string('positions', 'quiz_statistics'), $question->_stats->positions);
206     
207             $questionstatstable = new object();
208             $questionstatstable->align = array('center', 'center');
209             $questionstatstable->width = '60%';
210             $questionstatstable->class = 'generaltable titlesleft';
211     
212             unset($datumfromtable['number']);
213             unset($datumfromtable['icon']);
214             $actions = $datumfromtable['actions'];
215             unset($datumfromtable['actions']);
216             unset($datumfromtable['name']);
217             $labels = array('s' => get_string('attempts', 'quiz_statistics'),
218                             'facility' => get_string('facility', 'quiz_statistics'),
219                             'sd' => get_string('standarddeviationq', 'quiz_statistics'),
220                             'random_guess_score' => get_string('random_guess_score', 'quiz_statistics'),
221                             'intended_weight'=> get_string('intended_weight', 'quiz_statistics'),
222                             'effective_weight'=> get_string('effective_weight', 'quiz_statistics'),
223                             'discrimination_index'=> get_string('discrimination_index', 'quiz_statistics'),
224                             'discriminative_efficiency'=> get_string('discriminative_efficiency', 'quiz_statistics'));
225             foreach ($datumfromtable as $item => $value){
226                 $questionstatstable->data[] = array($labels[$item], $value);
227             }
228             print_heading(get_string('questioninformation', 'quiz_statistics'));
229             print_table($questioninfotable);
230             
231             print_box(format_text($question->questiontext, $question->questiontextformat).$actions, 'boxaligncenter generalbox boxwidthnormal mdl-align');
232     
233             print_heading(get_string('questionstatistics', 'quiz_statistics'));
234             print_table($questionstatstable);
235             
236         } else {
237             $this->qtable->export_class_instance($this->table->export_class_instance());
238             $questiontabletitle = !empty($question->number)?'('.$question->number.') ':'';
239             $questiontabletitle .= "\"{$question->name}\"";
240             $questiontabletitle = "<em>$questiontabletitle</em>";
241             if ($downloadtype == 'xhtml'){
242                 $questiontabletitle = get_string('analysisofresponsesfor', 'quiz_statistics', $questiontabletitle);
243             }
244             $exportclass =& $this->table->export_class_instance();
245             $exportclass->start_table($questiontabletitle);
246         }
247         if ($QTYPES[$question->qtype]->show_analysis_of_responses()){
248             if (!$this->table->is_downloading()){
249                 print_heading(get_string('analysisofresponses', 'quiz_statistics'));
250             }
251             $teacherresponses = $QTYPES[$question->qtype]->get_possible_responses($question);
252             $this->qtable->setup($reporturl, $question, count($teacherresponses)>1);
253             if ($this->table->is_downloading()){
254                 $exportclass->output_headers($this->qtable->headers);
255             }
256     
257             $responses = $DB->get_records('quiz_question_response_stats', array('quizstatisticsid' => $quizstats->id, 'questionid' => $question->id), 'credit DESC, subqid ASC, aid ASC, rcount DESC');
258             $responses = quiz_report_index_by_keys($responses, array('subqid', 'aid'), false);
259             foreach ($responses as $subqid => $response){
260                 foreach (array_keys($responses[$subqid]) as $aid){
261                     uasort($responses[$subqid][$aid], array('quiz_statistics_report', 'sort_answers'));
262                 }
263                 if (isset($responses[$subqid]['0'])){
264                     $wildcardresponse = new object();
265                     $wildcardresponse->answer = '*';
266                     $wildcardresponse->credit = 0;
267                     $teacherresponses[$subqid][0] = $wildcardresponse;
268                 }
269             }
270             $first = true;
271             $subq = 0;
272             foreach ($teacherresponses as $subqid => $tresponsesforsubq){
273                 $subq++;
274                 $qhaswildcards = $QTYPES[$question->qtype]->has_wildcards_in_responses($question, $subqid);
275                 if (!$first){
276                     $this->qtable->add_separator();
277                 }
278                 uasort($tresponsesforsubq, array('quiz_statistics_report', 'sort_response_details'));
279                 foreach ($tresponsesforsubq as $aid => $teacherresponse){
280                     $teacherresponserow = new object();
281                     $teacherresponserow->response = $teacherresponse->answer; 
282                     $teacherresponserow->rcount = 0;
283                     $teacherresponserow->subq = $subq;
284                     $teacherresponserow->credit = $teacherresponse->credit;
285                     if (isset($responses[$subqid][$aid])){
286                         $singleanswer = count($responses[$subqid][$aid])==1 && 
287                                         ($responses[$subqid][$aid][0]->response == $teacherresponserow->response);
288                         if (!$singleanswer && $qhaswildcards){
289                             $this->qtable->add_separator();
290                         }
291                         foreach ($responses[$subqid][$aid] as $response){
292                             $teacherresponserow->rcount += $response->rcount;
293                         }
294                         if ($aid!=0 || $qhaswildcards){
295                             $this->qtable->add_data_keyed($this->qtable->format_row($teacherresponserow));
296                         }
297                         if (!$singleanswer){
298                             foreach ($responses[$subqid][$aid] as $response){
299                                 if (!$downloadtype || $downloadtype=='xhtml'){
300                                     $indent = '&nbsp;&nbsp;&nbsp;&nbsp;';
301                                 } else {
302                                     $indent = '    ';
303                                 }
304                                 $response->response = ($qhaswildcards?$indent:'').$response->response;
305                                 $response->subq = $subq;
306                                 if ((count($responses[$subqid][$aid])<2) || ($response->rcount > ($teacherresponserow->rcount / 10))){
307                                     $this->qtable->add_data_keyed($this->qtable->format_row($response));
308                                 }
309                             }
310                         }
311                     } else {
312                         $this->qtable->add_data_keyed($this->qtable->format_row($teacherresponserow));
313                     }
314                 }
315                 $first = false;
316             }
317             $this->qtable->finish_output(!$this->table->is_downloading());
318         }
319         if (!$this->table->is_downloading()){
320             $url = $reporturl->out();
321             $text = get_string('backtoquizreport', 'quiz_statistics');
322             print_box("<a href=\"$url\">$text</a>", 'boxaligncenter generalbox boxwidthnormal mdl-align');
323         }
324     }
325         
326     function output_quiz_structure_analysis_table($s, $questions, $subquestions){
327         if ($s){
328             if (!$this->table->is_downloading()){
329                 print_heading(get_string('quizstructureanalysis', 'quiz_statistics'));
330             }
331             foreach ($questions as $question){
332                 $this->table->add_data_keyed($this->table->format_row($question));
333                 if (!empty($question->_stats->subquestions)){
334                     $subitemstodisplay = explode(',', $question->_stats->subquestions);
335                     foreach ($subitemstodisplay as $subitemid){
336                         $subquestions[$subitemid]->maxgrade = $question->maxgrade;
337                         $this->table->add_data_keyed($this->table->format_row($subquestions[$subitemid]));
338                     }
339                 }
340             }
342             $this->table->finish_output(!$this->table->is_downloading());
343         }
344     }
345     
346     function output_quiz_info_table($course, $cm, $quiz, $quizstats, $usingattemptsstring,
347                     $currentgroup, $groupstudents, $useallattempts, $download, $reporturl, $everything){
348         global $DB;
349         // Print information on the number of existing attempts
350         $quizinformationtablehtml = print_heading(get_string('quizinformation', 'quiz_statistics'), '', 2, 'main', true);
351         $quizinformationtable = new object();
352         $quizinformationtable->align = array('center', 'center');
353         $quizinformationtable->width = '60%';
354         $quizinformationtable->class = 'generaltable titlesleft';
355         $quizinformationtable->data = array();
356         $quizinformationtable->data[] = array(get_string('quizname', 'quiz_statistics'), $quiz->name);
357         $quizinformationtable->data[] = array(get_string('coursename', 'quiz_statistics'), $course->fullname);
358         if ($cm->idnumber){
359             $quizinformationtable->data[] = array(get_string('idnumbermod'), $cm->idnumber);
360         }
361         if ($quiz->timeopen){
362             $quizinformationtable->data[] = array(get_string('quizopen', 'quiz'), userdate($quiz->timeopen));
363         }
364         if ($quiz->timeclose){
365             $quizinformationtable->data[] = array(get_string('quizclose', 'quiz'), userdate($quiz->timeclose));
366         }
367         if ($quiz->timeopen && $quiz->timeclose){
368             $quizinformationtable->data[] = array(get_string('duration', 'quiz_statistics'), format_time($quiz->timeclose - $quiz->timeopen));
369         }
370         $format = array('firstattemptscount' => '',
371                     'allattemptscount' => '',
372                     'firstattemptsavg' => 'sumgrades_as_percentage',
373                     'allattemptsavg' => 'sumgrades_as_percentage',
374                     'median' => 'sumgrades_as_percentage',
375                     'standarddeviation' => 'sumgrades_as_percentage',
376                     'skewness' => '',
377                     'kurtosis' => '',
378                     'cic' => 'number_format',
379                     'errorratio' => 'number_format',
380                     'standarderror' => 'sumgrades_as_percentage');
381         foreach ($quizstats as $property => $value){
382             if (!isset($format[$property])){
383                 continue;
384             }
385             if (!is_null($value)){
386                 switch ($format[$property]){
387                     case 'sumgrades_as_percentage' :
388                         $formattedvalue = quiz_report_scale_sumgrades_as_percentage($value, $quiz);
389                         break;
390                     case 'number_format' :
391                         $formattedvalue = quiz_format_grade($quiz, $value).'%';
392                         break;
393                     default :
394                         $formattedvalue = $value;
395                 }
396                 $quizinformationtable->data[] = array(get_string($property, 'quiz_statistics', $usingattemptsstring), $formattedvalue);
397             }
398         }
399         if (!$this->table->is_downloading()){
400             if (isset($quizstats->timemodified)){
401                 list($fromqa, $whereqa, $qaparams) = quiz_report_attempts_sql($quiz->id, $currentgroup, $groupstudents, $useallattempts);
402                 $sql = 'SELECT COUNT(1) ' .
403                     'FROM ' .$fromqa.' '.
404                     'WHERE ' .$whereqa.' AND qa.timefinish > :time';
405                 $a = new object();
406                 $a->lastcalculated = format_time(time() - $quizstats->timemodified);
407                 if (!$a->count = $DB->count_records_sql($sql, array('time'=>$quizstats->timemodified)+$qaparams)){
408                     $a->count = 0;
409                 }
410                 $quizinformationtablehtml .= print_box_start('boxaligncenter generalbox boxwidthnormal mdl-align', '', true);
411                 $quizinformationtablehtml .= get_string('lastcalculated', 'quiz_statistics', $a);
412                 $quizinformationtablehtml .= print_single_button($reporturl->out(true), $reporturl->params()+array('recalculate'=>1),
413                                     get_string('recalculatenow', 'quiz_statistics'), 'post', '', true);
414                 $quizinformationtablehtml .= print_box_end(true);
415             }
416             $downloadoptions = $this->table->get_download_menu();
417             $quizinformationtablehtml .= '<form action="'. $this->table->baseurl .'" method="post">';
418             $quizinformationtablehtml .= '<div class="mdl-align">';
419             $quizinformationtablehtml .= '<input type="hidden" name="everything" value="1"/>';
420             $quizinformationtablehtml .= '<input type="submit" value="'.get_string('downloadeverything', 'quiz_statistics').'"/>';
421             $quizinformationtablehtml .= choose_from_menu ($downloadoptions, 'download', $this->table->defaultdownloadformat, '', '', '', true);
422             $quizinformationtablehtml .= helpbutton('tableexportformats', get_string('tableexportformats', 'table'), 'moodle', true, false, '', true);
423             $quizinformationtablehtml .= '</div></form>';
424         }
425         $quizinformationtablehtml .= print_table($quizinformationtable, true);
426         if (!$this->table->is_downloading()){
427             echo $quizinformationtablehtml;
428         } elseif ($everything) {
429             $exportclass =& $this->table->export_class_instance();
430             if ($download == 'xhtml'){
431                 echo $quizinformationtablehtml;
432             } else {
433                 $exportclass->start_table(get_string('quizinformation', 'quiz_statistics'));
434                 $headers = array();
435                 $row = array();
436                 foreach ($quizinformationtable->data as $data){
437                     $headers[]= $data[0];
438                     $row[] = $data[1];
439                 }
440                 $exportclass->output_headers($headers);
441                 $exportclass->add_data($row);
442                 $exportclass->finish_table();
443             }
444         }
445     }
447     function quiz_stats($nostudentsingroup, $quizid, $currentgroup, $groupstudents, $questions, $useallattempts){
448         global $CFG, $DB;
449         if (!$nostudentsingroup){
450             //Calculating_MEAN_of_grades_for_all_attempts_by_students
451             //http://docs.moodle.org/en/Development:Quiz_item_analysis_calculations_in_practise#Calculating_MEAN_of_grades_for_all_attempts_by_students
452         
453             list($fromqa, $whereqa, $qaparams) = quiz_report_attempts_sql($quizid, $currentgroup, $groupstudents);
454     
455             $sql = 'SELECT (CASE WHEN attempt=1 THEN 1 ELSE 0 END) AS isfirst, COUNT(1) AS countrecs, SUM(sumgrades) AS total ' .
456                     'FROM '.$fromqa.
457                     'WHERE ' .$whereqa.
458                     'GROUP BY (attempt=1)';
459             
460             if (!$attempttotals = $DB->get_records_sql($sql, $qaparams)){
461                 $s = 0;
462                 $usingattemptsstring = '';
463             } else {
464                 $firstattempt = $attempttotals[1];
465                 $allattempts = new object();
466                 $allattempts->countrecs = $firstattempt->countrecs + 
467                                 (isset($attempttotals[0])?$attempttotals[0]->countrecs:0);
468                 $allattempts->total = $firstattempt->total + 
469                                 (isset($attempttotals[0])?$attempttotals[0]->total:0);
470                 if ($useallattempts){
471                     $usingattempts = $allattempts;
472                     $usingattempts->attempts = get_string('allattempts', 'quiz_statistics');
473                     $usingattempts->sql = '';
474                 } else {
475                     $usingattempts = $firstattempt;
476                     $usingattempts->attempts = get_string('firstattempts', 'quiz_statistics');
477                     $usingattempts->sql = 'AND qa.attempt=1 ';
478                 }
479                 $usingattemptsstring = $usingattempts->attempts;
480                 $s = $usingattempts->countrecs;
481                 $sumgradesavg = $usingattempts->total / $usingattempts->countrecs;
482             }
483         } else {
484             $s = 0;
485         }
486         $quizstats = new object();
487         if ($s == 0){
488             $quizstats->firstattemptscount = 0;
489             $quizstats->allattemptscount = 0;
490         } else {
491             $quizstats->firstattemptscount = $firstattempt->countrecs;
492             $quizstats->allattemptscount = $allattempts->countrecs;
493             $quizstats->firstattemptsavg = $firstattempt->total / $firstattempt->countrecs;
494             $quizstats->allattemptsavg = $allattempts->total / $allattempts->countrecs;
495         }
496         //recalculate sql again this time possibly including test for first attempt.
497         list($fromqa, $whereqa, $qaparams) = quiz_report_attempts_sql($quizid, $currentgroup, $groupstudents, $useallattempts);
498         
499         //get the median
500         if ($s) {
502             if (($s%2)==0){
503                 //even number of attempts
504                 $limitoffset = ($s/2) - 1;
505                 $limit = 2;
506             } else {
507                 $limitoffset = (floor($s/2));
508                 $limit = 1;
509             }
510             $sql = 'SELECT id, sumgrades ' .
511                 'FROM ' .$fromqa.
512                 'WHERE ' .$whereqa.
513                 'ORDER BY sumgrades';
514             if (!$mediangrades = $DB->get_records_sql_menu($sql, $qaparams, $limitoffset, $limit)){
515                 print_error('errormedian', 'quiz_statistics');
516             }
517             $quizstats->median = array_sum($mediangrades) / count($mediangrades);
518             if ($s>1){
519                 //fetch sum of squared, cubed and power 4d 
520                 //differences between grades and mean grade
521                 $mean = $usingattempts->total / $s;
522                 $sql = "SELECT " .
523                     "SUM(POWER((qa.sumgrades - :mean1),2)) AS power2, " .
524                     "SUM(POWER((qa.sumgrades - :mean2),3)) AS power3, ".
525                     "SUM(POWER((qa.sumgrades - :mean3),4)) AS power4 ".
526                     'FROM ' .$fromqa.
527                     'WHERE ' .$whereqa;
528                 $params = array('mean1' => $mean, 'mean2' => $mean, 'mean3' => $mean)+$qaparams;
529                 if (!$powers = $DB->get_record_sql($sql, $params)){
530                     print_error('errorpowers', 'quiz_statistics');
531                 }
532                 
533                 //Standard_Deviation
534                 //see http://docs.moodle.org/en/Development:Quiz_item_analysis_calculations_in_practise#Standard_Deviation
535                 
536                 $quizstats->standarddeviation = sqrt($powers->power2 / ($s -1));
537                 
539                 
540                 //Skewness_and_Kurtosis
541                 if ($s>2){
542                     //see http://docs.moodle.org/en/Development:Quiz_item_analysis_calculations_in_practise#Skewness_and_Kurtosis
543                     $m2= $powers->power2 / $s;
544                     $m3= $powers->power3 / $s;
545                     $m4= $powers->power4 / $s;
546                     
547                     $k2= $s*$m2/($s-1);
548                     $k3= $s*$s*$m3/(($s-1)*($s-2));
549                     if ($k2){
550                         $quizstats->skewness = $k3 / (pow($k2, 3/2));
551                     }
552                 }
553     
554     
555                 if ($s>3){
556                     $k4= $s*$s*((($s+1)*$m4)-(3*($s-1)*$m2*$m2))/(($s-1)*($s-2)*($s-3));
557                     if ($k2){
558                         $quizstats->kurtosis = $k4 / ($k2*$k2);
559                     }
560                 }
561             }
562         }
563         if ($s){
564             require_once("$CFG->dirroot/mod/quiz/report/statistics/qstats.php");
565             $qstats = new qstats($questions, $s, $sumgradesavg);
566             $qstats->get_records($quizid, $currentgroup, $groupstudents, $useallattempts);
567             $qstats->process_states();
568             $qstats->process_responses();
569         } else {
570             $qstats = false;
571         }
572         if ($s>1){
573             $p = count($qstats->questions);//no of positions
574             if ($p > 1){
575                 if (isset($k2)){
576                     $quizstats->cic = (100 * $p / ($p -1)) * (1 - ($qstats->sum_of_grade_variance())/$k2);
577                     $quizstats->errorratio = 100 * sqrt(1-($quizstats->cic/100));
578                     $quizstats->standarderror = ($quizstats->errorratio * $quizstats->standarddeviation / 100);
579                 }
580             }
581         }
582         return array($s, $usingattemptsstring, $quizstats, $qstats);
583     }
584     
585     function quiz_questions_stats($quiz, $currentgroup, $nostudentsingroup, $useallattempts, $groupstudents, $questions){
586         global $DB;
587         $timemodified = time() - QUIZ_REPORT_TIME_TO_CACHE_STATS;
588         $params = array('quizid'=>$quiz->id, 'groupid'=>(int)$currentgroup, 'allattempts'=>$useallattempts, 'timemodified'=>$timemodified);
589         if (!$quizstats = $DB->get_record_select('quiz_statistics', 'quizid = :quizid  AND groupid = :groupid AND allattempts = :allattempts AND timemodified > :timemodified', $params, '*', true)){
590             list($s, $usingattemptsstring, $quizstats, $qstats) = $this->quiz_stats($nostudentsingroup, $quiz->id, $currentgroup, $groupstudents, $questions, $useallattempts);
591             if ($s){
592                 $toinsert = (object)((array)$quizstats + $params);
593                 $toinsert->timemodified = time();
594                 $quizstats->id = $DB->insert_record('quiz_statistics', $toinsert);
595                 foreach ($qstats->questions as $question){
596                     $question->_stats->quizstatisticsid = $quizstats->id;
597                     $DB->insert_record('quiz_question_statistics', $question->_stats, false, true);
598                 }
599                 foreach ($qstats->subquestions as $subquestion){
600                     $subquestion->_stats->quizstatisticsid = $quizstats->id;
601                     $DB->insert_record('quiz_question_statistics', $subquestion->_stats, false, true);
602                 }
603                 foreach ($qstats->responses as $response){
604                     $response->quizstatisticsid = $quizstats->id;
605                     $DB->insert_record('quiz_question_response_stats', $response, false);
606                 }
607             }
608             if ($qstats){
609                 $questions = $qstats->questions;
610                 $subquestions = $qstats->subquestions;
611             } else {
612                 $questions = array();
613                 $subquestions = array();
614             }
615         } else {
616             //use cached results
617             if ($useallattempts){
618                 $usingattemptsstring = get_string('allattempts', 'quiz_statistics');
619                 $s = $quizstats->allattemptscount;
620             } else {
621                 $usingattemptsstring = get_string('firstattempts', 'quiz_statistics');
622                 $s = $quizstats->firstattemptscount;
623             }
624             $subquestions = array();
625             $questionstats = $DB->get_records('quiz_question_statistics', array('quizstatisticsid'=>$quizstats->id), 'subquestion ASC');
626             $questionstats = quiz_report_index_by_keys($questionstats, array('subquestion', 'questionid'));
627             if (1 < count($questionstats)){
628                 list($mainquestionstats, $subquestionstats) = $questionstats;
629                 $subqstofetch = array_keys($subquestionstats);
630                 $subquestions = question_load_questions($subqstofetch);
631                 foreach (array_keys($subquestions) as $subqid){
632                     $subquestions[$subqid]->_stats = $subquestionstats[$subqid];
633                 }
634             } elseif (count($questionstats)) {
635                 $mainquestionstats = $questionstats[0];
636             }
637             if (count($questionstats)) {
638                 foreach (array_keys($questions) as $qid){
639                     $questions[$qid]->_stats = $mainquestionstats[$qid];
640                 }
641             }
642         }
643         return array($quizstats, $questions, $subquestions, $s, $usingattemptsstring);
644     }
646 function quiz_report_attempts_sql($quizid, $currentgroup, $groupstudents, $allattempts = true){
647     global $DB;
648     $fromqa = '{quiz_attempts} qa ';
649     $whereqa = 'qa.quiz = :quizid AND qa.preview=0 AND qa.timefinish !=0 ';
650     $qaparams = array('quizid'=>$quizid);
651     if (!empty($currentgroup) && $groupstudents) {
652         list($grpsql, $grpparams) = $DB->get_in_or_equal(array_keys($groupstudents), SQL_PARAMS_NAMED, 'u0000');
653         $whereqa .= 'AND qa.userid '.$grpsql.' ';
654         $qaparams += $grpparams;
655     }
656     if (!$allattempts){
657         $whereqa .= 'AND qa.attempt=1 ';
658     }
659     return array($fromqa, $whereqa, $qaparams);
662 ?>