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