weekly release 2.6dev
[moodle.git] / mod / quiz / report / statistics / report.php
CommitLineData
0c1c764e 1<?php
04853f27
TH
2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16
0c1c764e 17/**
04853f27 18 * Quiz statistics report class.
0c1c764e 19 *
8d76124c
TH
20 * @package quiz_statistics
21 * @copyright 2008 Jamie Pratt
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
04853f27
TH
23 */
24
0c1c764e 25
a17b297d
TH
26defined('MOODLE_INTERNAL') || die();
27
04853f27
TH
28require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_form.php');
29require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_table.php');
30require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_question_table.php');
31require_once($CFG->dirroot . '/mod/quiz/report/statistics/qstats.php');
32require_once($CFG->dirroot . '/mod/quiz/report/statistics/responseanalysis.php');
0c1c764e 33
a17b297d 34
04853f27
TH
35/**
36 * The quiz statistics report provides summary information about each question in
37 * a quiz, compared to the whole quiz. It also provides a drill-down to more
38 * detailed information about each question.
39 *
8d76124c
TH
40 * @copyright 2008 Jamie Pratt
41 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
04853f27 42 */
c386eaa3 43class quiz_statistics_report extends quiz_default_report {
04853f27 44 /** @var integer Time after which statistics are automatically recomputed. */
768a7588 45 const TIME_TO_CACHE_STATS = 900; // 15 minutes.
3b1d5cc4 46
04853f27
TH
47 /** @var object instance of table class used for main questions stats table. */
48 protected $table;
0c1c764e 49
50 /**
51 * Display the report.
52 */
04853f27
TH
53 public function display($quiz, $cm, $course) {
54 global $CFG, $DB, $OUTPUT, $PAGE;
0c1c764e 55
26aded55 56 $this->context = context_module::instance($cm->id);
0c1c764e 57
04853f27 58 // Work out the display options.
0c1c764e 59 $download = optional_param('download', '', PARAM_ALPHA);
869309b8 60 $everything = optional_param('everything', 0, PARAM_BOOL);
d1789d5d 61 $recalculate = optional_param('recalculate', 0, PARAM_BOOL);
04853f27 62 // A qid paramter indicates we should display the detailed analysis of a question.
43ec99aa 63 $qid = optional_param('qid', 0, PARAM_INT);
04853f27
TH
64 $slot = optional_param('slot', 0, PARAM_INT);
65
0c1c764e 66 $pageoptions = array();
67 $pageoptions['id'] = $cm->id;
0c1c764e 68 $pageoptions['mode'] = 'statistics';
69
a6855934 70 $reporturl = new moodle_url('/mod/quiz/report.php', $pageoptions);
0c1c764e 71
6b4e2d76 72 $mform = new quiz_statistics_settings_form($reporturl);
04853f27 73 if ($fromform = $mform->get_data()) {
0c1c764e 74 $useallattempts = $fromform->useallattempts;
04853f27 75 if ($fromform->useallattempts) {
25a03faa
TH
76 set_user_preference('quiz_report_statistics_useallattempts',
77 $fromform->useallattempts);
0c1c764e 78 } else {
79 unset_user_preference('quiz_report_statistics_useallattempts');
80 }
04853f27 81
0c1c764e 82 } else {
83 $useallattempts = get_user_preferences('quiz_report_statistics_useallattempts', 0);
84 }
85
768a7588 86 // Find out current groups mode.
490668bb 87 $currentgroup = $this->get_current_group($cm, $course, $this->context);
04853f27
TH
88 $nostudentsingroup = false; // True if a group is selected and there is no one in it.
89 if (empty($currentgroup)) {
90 $currentgroup = 0;
91 $groupstudents = array();
3b1d5cc4 92
e4977ba5
TH
93 } else if ($currentgroup == self::NO_GROUPS_ALLOWED) {
94 $groupstudents = array();
95 $nostudentsingroup = true;
96
04853f27 97 } else {
768a7588 98 // All users who can attempt quizzes and who are in the currently selected group.
fdb5bc03 99 $groupstudents = get_users_by_capability($this->context,
04853f27
TH
100 array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'),
101 '', '', '', '', $currentgroup, '', false);
102 if (!$groupstudents) {
e72efdd4 103 $nostudentsingroup = true;
0c1c764e 104 }
0c1c764e 105 }
3b1d5cc4 106
04853f27 107 // If recalculate was requested, handle that.
db77f410 108 if ($recalculate && confirm_sesskey()) {
04853f27 109 $this->clear_cached_data($quiz->id, $currentgroup, $useallattempts);
e42f153c 110 redirect($reporturl);
43ec99aa 111 }
3b1d5cc4 112
04853f27 113 // Set up the main table.
59ea8176 114 $this->table = new quiz_statistics_table();
04853f27
TH
115 if ($everything) {
116 $report = get_string('completestatsfilename', 'quiz_statistics');
117 } else {
118 $report = get_string('questionstatsfilename', 'quiz_statistics');
119 }
0eafc988 120 $courseshortname = format_string($course->shortname, true,
26aded55 121 array('context' => context_course::instance($course->id)));
8ebbb06a 122 $filename = quiz_report_download_filename($report, $courseshortname, $quiz->name);
25a03faa
TH
123 $this->table->is_downloading($download, $filename,
124 get_string('quizstructureanalysis', 'quiz_statistics'));
3652dddd 125 $questions = $this->load_and_initialise_questions_for_calculations($quiz);
04853f27
TH
126
127 // Get the data to be displayed.
128 list($quizstats, $questions, $subquestions, $s) =
129 $this->get_quiz_and_questions_stats($quiz, $currentgroup,
130 $nostudentsingroup, $useallattempts, $groupstudents, $questions);
131 $quizinfo = $this->get_formatted_quiz_info_data($course, $cm, $quiz, $quizstats);
132
04853f27 133 // Set up the table, if there is data.
b0e4fa41 134 if ($s) {
90fbad64 135 $this->table->statistics_setup($quiz, $cm->id, $reporturl, $s);
43ec99aa 136 }
3b1d5cc4 137
04853f27
TH
138 // Print the page header stuff (if not downloading.
139 if (!$this->table->is_downloading()) {
140 $this->print_header_and_tabs($cm, $course, $quiz, 'statistics');
141
e4977ba5 142 if (groups_get_activity_groupmode($cm)) {
04853f27
TH
143 groups_print_activity_menu($cm, $reporturl->out());
144 if ($currentgroup && !$groupstudents) {
145 $OUTPUT->notification(get_string('nostudentsingroup', 'quiz_statistics'));
b0e4fa41
TH
146 }
147 }
148
3c6185e9 149 if (!quiz_questions_in_quiz($quiz->questions)) {
fdb5bc03 150 echo quiz_no_questions_message($quiz, $cm, $this->context);
3c6185e9
TH
151 } else if (!$this->table->is_downloading() && $s == 0) {
152 echo $OUTPUT->notification(get_string('noattempts', 'quiz'));
153 }
154
04853f27
TH
155 // Print display options form.
156 $mform->set_data(array('useallattempts' => $useallattempts));
157 $mform->display();
158 }
159
160 if ($everything) { // Implies is downloading.
161 // Overall report, then the analysis of each question.
162 $this->download_quiz_info_table($quizinfo);
163
164 if ($s) {
165 $this->output_quiz_structure_analysis_table($s, $questions, $subquestions);
166
167 if ($this->table->is_downloading() == 'xhtml') {
168 $this->output_statistics_graph($quizstats->id, $s);
869309b8 169 }
04853f27
TH
170
171 foreach ($questions as $question) {
25a03faa
TH
172 if (question_bank::get_qtype(
173 $question->qtype, false)->can_analyse_responses()) {
04853f27
TH
174 $this->output_individual_question_response_analysis(
175 $question, $reporturl, $quizstats);
176
177 } else if (!empty($question->_stats->subquestions)) {
178 $subitemstodisplay = explode(',', $question->_stats->subquestions);
179 foreach ($subitemstodisplay as $subitemid) {
180 $this->output_individual_question_response_analysis(
181 $subquestions[$subitemid], $reporturl, $quizstats);
869309b8 182 }
183 }
869309b8 184 }
185 }
04853f27
TH
186
187 $this->table->export_class_instance()->finish_document();
188
189 } else if ($slot) {
190 // Report on an individual question indexed by position.
191 if (!isset($questions[$slot])) {
192 print_error('questiondoesnotexist', 'question');
869309b8 193 }
04853f27
TH
194
195 $this->output_individual_question_data($quiz, $questions[$slot]);
196 $this->output_individual_question_response_analysis(
197 $questions[$slot], $reporturl, $quizstats);
198
199 // Back to overview link.
200 echo $OUTPUT->box('<a href="' . $reporturl->out() . '">' .
201 get_string('backtoquizreport', 'quiz_statistics') . '</a>',
2f1ba2ae 202 'backtomainstats boxaligncenter generalbox boxwidthnormal mdl-align');
04853f27
TH
203
204 } else if ($qid) {
205 // Report on an individual sub-question indexed questionid.
206 if (!isset($subquestions[$qid])) {
43ec99aa 207 print_error('questiondoesnotexist', 'question');
71a2b878 208 }
04853f27
TH
209
210 $this->output_individual_question_data($quiz, $subquestions[$qid]);
211 $this->output_individual_question_response_analysis(
212 $subquestions[$qid], $reporturl, $quizstats);
213
214 // Back to overview link.
215 echo $OUTPUT->box('<a href="' . $reporturl->out() . '">' .
216 get_string('backtoquizreport', 'quiz_statistics') . '</a>',
217 'boxaligncenter generalbox boxwidthnormal mdl-align');
218
219 } else if ($this->table->is_downloading()) {
220 // Downloading overview report.
221 $this->download_quiz_info_table($quizinfo);
222 $this->output_quiz_structure_analysis_table($s, $questions, $subquestions);
223 $this->table->finish_output();
224
225 } else {
226 // On-screen display of overview report.
227 echo $OUTPUT->heading(get_string('quizinformation', 'quiz_statistics'));
25a03faa
TH
228 echo $this->output_caching_info($quizstats, $quiz->id, $currentgroup,
229 $groupstudents, $useallattempts, $reporturl);
04853f27
TH
230 echo $this->everything_download_options();
231 echo $this->output_quiz_info_table($quizinfo);
232 if ($s) {
233 echo $OUTPUT->heading(get_string('quizstructureanalysis', 'quiz_statistics'));
234 $this->output_quiz_structure_analysis_table($s, $questions, $subquestions);
235 $this->output_statistics_graph($quizstats->id, $s);
b0e4fa41 236 }
43ec99aa 237 }
04853f27 238
43ec99aa 239 return true;
240 }
3b1d5cc4 241
04853f27
TH
242 /**
243 * Display the statistical and introductory information about a question.
244 * Only called when not downloading.
245 * @param object $quiz the quiz settings.
246 * @param object $question the question to report on.
247 * @param moodle_url $reporturl the URL to resisplay this report.
248 * @param object $quizstats Holds the quiz statistics.
249 */
250 protected function output_individual_question_data($quiz, $question) {
251 global $OUTPUT;
252
253 // On-screen display. Show a summary of the question's place in the quiz,
254 // and the question statistics.
255 $datumfromtable = $this->table->format_row($question);
256
257 // Set up the question info table.
258 $questioninfotable = new html_table();
259 $questioninfotable->align = array('center', 'center');
260 $questioninfotable->width = '60%';
261 $questioninfotable->attributes['class'] = 'generaltable titlesleft';
262
263 $questioninfotable->data = array();
264 $questioninfotable->data[] = array(get_string('modulename', 'quiz'), $quiz->name);
265 $questioninfotable->data[] = array(get_string('questionname', 'quiz_statistics'),
266 $question->name.'&nbsp;'.$datumfromtable['actions']);
267 $questioninfotable->data[] = array(get_string('questiontype', 'quiz_statistics'),
268 $datumfromtable['icon'] . '&nbsp;' .
269 question_bank::get_qtype($question->qtype, false)->menu_name() . '&nbsp;' .
270 $datumfromtable['icon']);
271 $questioninfotable->data[] = array(get_string('positions', 'quiz_statistics'),
272 $question->_stats->positions);
273
274 // Set up the question statistics table.
275 $questionstatstable = new html_table();
276 $questionstatstable->align = array('center', 'center');
277 $questionstatstable->width = '60%';
278 $questionstatstable->attributes['class'] = 'generaltable titlesleft';
279
280 unset($datumfromtable['number']);
281 unset($datumfromtable['icon']);
282 $actions = $datumfromtable['actions'];
283 unset($datumfromtable['actions']);
284 unset($datumfromtable['name']);
25a03faa
TH
285 $labels = array(
286 's' => get_string('attempts', 'quiz_statistics'),
287 'facility' => get_string('facility', 'quiz_statistics'),
288 'sd' => get_string('standarddeviationq', 'quiz_statistics'),
289 'random_guess_score' => get_string('random_guess_score', 'quiz_statistics'),
290 'intended_weight' => get_string('intended_weight', 'quiz_statistics'),
291 'effective_weight' => get_string('effective_weight', 'quiz_statistics'),
292 'discrimination_index' => get_string('discrimination_index', 'quiz_statistics'),
293 'discriminative_efficiency' =>
294 get_string('discriminative_efficiency', 'quiz_statistics')
295 );
04853f27
TH
296 foreach ($datumfromtable as $item => $value) {
297 $questionstatstable->data[] = array($labels[$item], $value);
869309b8 298 }
04853f27
TH
299
300 // Display the various bits.
301 echo $OUTPUT->heading(get_string('questioninformation', 'quiz_statistics'));
302 echo html_writer::table($questioninfotable);
edfa0d80 303 echo $this->render_question_text($question);
04853f27
TH
304 echo $OUTPUT->heading(get_string('questionstatistics', 'quiz_statistics'));
305 echo html_writer::table($questionstatstable);
869309b8 306 }
3b1d5cc4 307
edfa0d80
TH
308 /**
309 * @param object $question question data.
310 * @return string HTML of question text, ready for display.
311 */
fdb5bc03 312 protected function render_question_text($question) {
edfa0d80 313 global $OUTPUT;
fdb5bc03 314
68d2f6a0
TH
315 $text = question_rewrite_question_preview_urls($question->questiontext, $question->id,
316 $question->contextid, 'question', 'questiontext', $question->id,
317 $this->context->id, 'quiz_statistics');
fdb5bc03
TH
318
319 return $OUTPUT->box(format_text($text, $question->questiontextformat,
320 array('noclean' => true, 'para' => false, 'overflowdiv' => true)),
edfa0d80
TH
321 'questiontext boxaligncenter generalbox boxwidthnormal mdl-align');
322 }
323
04853f27
TH
324 /**
325 * Display the response analysis for a question.
326 * @param object $question the question to report on.
327 * @param moodle_url $reporturl the URL to resisplay this report.
328 * @param object $quizstats Holds the quiz statistics.
329 */
25a03faa
TH
330 protected function output_individual_question_response_analysis($question,
331 $reporturl, $quizstats) {
04853f27 332 global $OUTPUT;
3b1d5cc4 333
04853f27
TH
334 if (!question_bank::get_qtype($question->qtype, false)->can_analyse_responses()) {
335 return;
336 }
3b1d5cc4 337
59ea8176 338 $qtable = new quiz_statistics_question_table($question->id);
04853f27
TH
339 $exportclass = $this->table->export_class_instance();
340 $qtable->export_class_instance($exportclass);
341 if (!$this->table->is_downloading()) {
342 // Output an appropriate title.
343 echo $OUTPUT->heading(get_string('analysisofresponses', 'quiz_statistics'));
3b1d5cc4 344
869309b8 345 } else {
04853f27
TH
346 // Work out an appropriate title.
347 $questiontabletitle = '"' . $question->name . '"';
348 if (!empty($question->number)) {
349 $questiontabletitle = '(' . $question->number . ') ' . $questiontabletitle;
350 }
351 if ($this->table->is_downloading() == 'xhtml') {
25a03faa
TH
352 $questiontabletitle = get_string('analysisofresponsesfor',
353 'quiz_statistics', $questiontabletitle);
869309b8 354 }
04853f27
TH
355
356 // Set up the table.
869309b8 357 $exportclass->start_table($questiontabletitle);
edfa0d80
TH
358
359 if ($this->table->is_downloading() == 'xhtml') {
360 echo $this->render_question_text($question);
361 }
869309b8 362 }
3b1d5cc4 363
04853f27
TH
364 $responesstats = new quiz_statistics_response_analyser($question);
365 $responesstats->load_cached($quizstats->id);
366
90fbad64 367 $qtable->question_setup($reporturl, $question, $responesstats);
04853f27
TH
368 if ($this->table->is_downloading()) {
369 $exportclass->output_headers($qtable->headers);
370 }
371
372 foreach ($responesstats->responseclasses as $partid => $partclasses) {
0ff4bd08 373 $rowdata = new stdClass();
04853f27
TH
374 $rowdata->part = $partid;
375 foreach ($partclasses as $responseclassid => $responseclass) {
376 $rowdata->responseclass = $responseclass->responseclass;
377
378 $responsesdata = $responesstats->responses[$partid][$responseclassid];
379 if (empty($responsesdata)) {
380 if (!array_key_exists('responseclass', $qtable->columns)) {
381 $rowdata->response = $responseclass->responseclass;
869309b8 382 } else {
04853f27 383 $rowdata->response = '';
869309b8 384 }
04853f27
TH
385 $rowdata->fraction = $responseclass->fraction;
386 $rowdata->count = 0;
387 $qtable->add_data_keyed($qtable->format_row($rowdata));
388 continue;
389 }
390
391 foreach ($responsesdata as $response => $data) {
392 $rowdata->response = $response;
393 $rowdata->fraction = $data->fraction;
394 $rowdata->count = $data->count;
395 $qtable->add_data_keyed($qtable->format_row($rowdata));
869309b8 396 }
869309b8 397 }
43ec99aa 398 }
04853f27
TH
399
400 $qtable->finish_output(!$this->table->is_downloading());
869309b8 401 }
3b1d5cc4 402
04853f27
TH
403 /**
404 * Output the table that lists all the questions in the quiz with their statistics.
f7970e3c 405 * @param int $s number of attempts.
04853f27
TH
406 * @param array $questions the questions in the quiz.
407 * @param array $subquestions the subquestions of any random questions.
408 */
409 protected function output_quiz_structure_analysis_table($s, $questions, $subquestions) {
410 if (!$s) {
411 return;
412 }
413
414 foreach ($questions as $question) {
415 // Output the data for this questions.
416 $this->table->add_data_keyed($this->table->format_row($question));
417
418 if (empty($question->_stats->subquestions)) {
419 continue;
71a2b878 420 }
43ec99aa 421
04853f27
TH
422 // And its subquestions, if it has any.
423 $subitemstodisplay = explode(',', $question->_stats->subquestions);
424 foreach ($subitemstodisplay as $subitemid) {
425 $subquestions[$subitemid]->maxmark = $question->maxmark;
426 $this->table->add_data_keyed($this->table->format_row($subquestions[$subitemid]));
427 }
0c1c764e 428 }
04853f27
TH
429
430 $this->table->finish_output(!$this->table->is_downloading());
43ec99aa 431 }
3b1d5cc4 432
04853f27
TH
433 protected function get_formatted_quiz_info_data($course, $cm, $quiz, $quizstats) {
434
435 // You can edit this array to control which statistics are displayed.
436 $todisplay = array('firstattemptscount' => 'number',
437 'allattemptscount' => 'number',
438 'firstattemptsavg' => 'summarks_as_percentage',
439 'allattemptsavg' => 'summarks_as_percentage',
440 'median' => 'summarks_as_percentage',
441 'standarddeviation' => 'summarks_as_percentage',
442 'skewness' => 'number_format',
443 'kurtosis' => 'number_format',
444 'cic' => 'number_format_percent',
445 'errorratio' => 'number_format_percent',
446 'standarderror' => 'summarks_as_percentage');
447
448 // General information about the quiz.
449 $quizinfo = array();
450 $quizinfo[get_string('quizname', 'quiz_statistics')] = format_string($quiz->name);
451 $quizinfo[get_string('coursename', 'quiz_statistics')] = format_string($course->fullname);
452 if ($cm->idnumber) {
453 $quizinfo[get_string('idnumbermod')] = $cm->idnumber;
454 }
455 if ($quiz->timeopen) {
456 $quizinfo[get_string('quizopen', 'quiz')] = userdate($quiz->timeopen);
457 }
458 if ($quiz->timeclose) {
459 $quizinfo[get_string('quizclose', 'quiz')] = userdate($quiz->timeclose);
460 }
461 if ($quiz->timeopen && $quiz->timeclose) {
25a03faa
TH
462 $quizinfo[get_string('duration', 'quiz_statistics')] =
463 format_time($quiz->timeclose - $quiz->timeopen);
04853f27
TH
464 }
465
466 // The statistics.
467 foreach ($todisplay as $property => $format) {
468 if (!isset($quizstats->$property) || empty($format[$property])) {
43ec99aa 469 continue;
71a2b878 470 }
04853f27
TH
471 $value = $quizstats->$property;
472
473 switch ($format) {
474 case 'summarks_as_percentage':
475 $formattedvalue = quiz_report_scale_summarks_as_percentage($value, $quiz);
476 break;
477 case 'number_format_percent':
478 $formattedvalue = quiz_format_grade($quiz, $value) . '%';
479 break;
480 case 'number_format':
768a7588 481 // 2 extra decimal places, since not a percentage,
25a03faa 482 // and we want the same number of sig figs.
04853f27
TH
483 $formattedvalue = format_float($value, $quiz->decimalpoints + 2);
484 break;
485 case 'number':
486 $formattedvalue = $value + 0;
487 break;
488 default:
489 $formattedvalue = $value;
71a2b878 490 }
04853f27
TH
491
492 $quizinfo[get_string($property, 'quiz_statistics',
493 $this->using_attempts_string(!empty($quizstats->allattempts)))] =
494 $formattedvalue;
43ec99aa 495 }
04853f27
TH
496
497 return $quizinfo;
498 }
499
500 /**
501 * Output the table of overall quiz statistics.
502 * @param array $quizinfo as returned by {@link get_formatted_quiz_info_data()}.
503 * @return string the HTML.
504 */
505 protected function output_quiz_info_table($quizinfo) {
506
507 $quizinfotable = new html_table();
508 $quizinfotable->align = array('center', 'center');
509 $quizinfotable->width = '60%';
510 $quizinfotable->attributes['class'] = 'generaltable titlesleft';
511 $quizinfotable->data = array();
512
513 foreach ($quizinfo as $heading => $value) {
514 $quizinfotable->data[] = array($heading, $value);
71a2b878 515 }
04853f27
TH
516
517 return html_writer::table($quizinfotable);
71a2b878 518 }
43ec99aa 519
04853f27
TH
520 /**
521 * Download the table of overall quiz statistics.
522 * @param array $quizinfo as returned by {@link get_formatted_quiz_info_data()}.
523 */
524 protected function download_quiz_info_table($quizinfo) {
525 global $OUTPUT;
3b1d5cc4 526
04853f27
TH
527 // XHTML download is a special case.
528 if ($this->table->is_downloading() == 'xhtml') {
529 echo $OUTPUT->heading(get_string('quizinformation', 'quiz_statistics'));
530 echo $this->output_quiz_info_table($quizinfo);
531 return;
532 }
3b1d5cc4 533
04853f27
TH
534 // Reformat the data ready for output.
535 $headers = array();
536 $row = array();
537 foreach ($quizinfo as $heading => $value) {
538 $headers[] = $heading;
539 $row[] = $value;
540 }
3b1d5cc4 541
04853f27
TH
542 // Do the output.
543 $exportclass = $this->table->export_class_instance();
544 $exportclass->start_table(get_string('quizinformation', 'quiz_statistics'));
545 $exportclass->output_headers($headers);
546 $exportclass->add_data($row);
547 $exportclass->finish_table();
548 }
549
550 /**
551 * Output the HTML needed to show the statistics graph.
f7970e3c 552 * @param int $quizstatsid the id of the statistics to show in the graph.
04853f27
TH
553 */
554 protected function output_statistics_graph($quizstatsid, $s) {
2cdcb905 555 global $PAGE;
04853f27
TH
556
557 if ($s == 0) {
558 return;
559 }
560
2cdcb905 561 $output = $PAGE->get_renderer('mod_quiz');
04853f27
TH
562 $imageurl = new moodle_url('/mod/quiz/report/statistics/statistics_graph.php',
563 array('id' => $quizstatsid));
2cdcb905
TH
564 $graphname = get_string('statisticsreportgraph', 'quiz_statistics');
565 echo $output->graph($imageurl, $graphname);
04853f27
TH
566 }
567
568 /**
569 * Return the stats data for when there are no stats to show.
570 *
571 * @param array $questions question definitions.
f7970e3c
TH
572 * @param int $firstattemptscount number of first attempts (optional).
573 * @param int $firstattemptscount total number of attempts (optional).
04853f27
TH
574 * @return array with three elements:
575 * - integer $s Number of attempts included in the stats (0).
576 * - array $quizstats The statistics for overall attempt scores.
577 * - array $qstats The statistics for each question.
578 */
579 protected function get_emtpy_stats($questions, $firstattemptscount = 0,
580 $allattemptscount = 0) {
0ff4bd08 581 $quizstats = new stdClass();
04853f27
TH
582 $quizstats->firstattemptscount = $firstattemptscount;
583 $quizstats->allattemptscount = $allattemptscount;
584
0ff4bd08 585 $qstats = new stdClass();
04853f27
TH
586 $qstats->questions = $questions;
587 $qstats->subquestions = array();
588 $qstats->responses = array();
589
590 return array(0, $quizstats, false);
591 }
592
593 /**
594 * Compute the quiz statistics.
595 *
3652dddd 596 * @param int $quizid the quiz id.
f7970e3c
TH
597 * @param int $currentgroup the current group. 0 for none.
598 * @param bool $nostudentsingroup true if there a no students.
599 * @param bool $useallattempts use all attempts, or just first attempts.
04853f27
TH
600 * @param array $groupstudents students in this group.
601 * @param array $questions question definitions.
602 * @return array with three elements:
603 * - integer $s Number of attempts included in the stats.
604 * - array $quizstats The statistics for overall attempt scores.
605 * - array $qstats The statistics for each question.
606 */
607 protected function compute_stats($quizid, $currentgroup, $nostudentsingroup,
608 $useallattempts, $groupstudents, $questions) {
609 global $DB;
610
611 // Calculating MEAN of marks for all attempts by students
728ebac7 612 // http://docs.moodle.org/dev/Quiz_item_analysis_calculations_in_practise
768a7588 613 // #Calculating_MEAN_of_grades_for_all_attempts_by_students.
04853f27
TH
614 if ($nostudentsingroup) {
615 return $this->get_emtpy_stats($questions);
616 }
617
618 list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql(
619 $quizid, $currentgroup, $groupstudents, true);
620
621 $attempttotals = $DB->get_records_sql("
622 SELECT
623 CASE WHEN attempt = 1 THEN 1 ELSE 0 END AS isfirst,
624 COUNT(1) AS countrecs,
625 SUM(sumgrades) AS total
626 FROM $fromqa
627 WHERE $whereqa
1ccfd8dc 628 GROUP BY CASE WHEN attempt = 1 THEN 1 ELSE 0 END", $qaparams);
04853f27
TH
629
630 if (!$attempttotals) {
631 return $this->get_emtpy_stats($questions);
632 }
633
634 if (isset($attempttotals[1])) {
635 $firstattempts = $attempttotals[1];
636 $firstattempts->average = $firstattempts->total / $firstattempts->countrecs;
0c1c764e 637 } else {
0ff4bd08 638 $firstattempts = new stdClass();
04853f27
TH
639 $firstattempts->countrecs = 0;
640 $firstattempts->total = 0;
41d5cbc1 641 $firstattempts->average = null;
0c1c764e 642 }
04853f27 643
0ff4bd08 644 $allattempts = new stdClass();
04853f27
TH
645 if (isset($attempttotals[0])) {
646 $allattempts->countrecs = $firstattempts->countrecs + $attempttotals[0]->countrecs;
647 $allattempts->total = $firstattempts->total + $attempttotals[0]->total;
71a2b878 648 } else {
04853f27
TH
649 $allattempts->countrecs = $firstattempts->countrecs;
650 $allattempts->total = $firstattempts->total;
0c1c764e 651 }
3b1d5cc4 652
04853f27
TH
653 if ($useallattempts) {
654 $usingattempts = $allattempts;
655 $usingattempts->sql = '';
656 } else {
657 $usingattempts = $firstattempts;
658 $usingattempts->sql = 'AND quiza.attempt = 1 ';
659 }
e72efdd4 660
04853f27
TH
661 $s = $usingattempts->countrecs;
662 if ($s == 0) {
663 return $this->get_emtpy_stats($questions, $firstattempts->countrecs,
664 $allattempts->countrecs);
665 }
666 $summarksavg = $usingattempts->total / $usingattempts->countrecs;
3b1d5cc4 667
0ff4bd08 668 $quizstats = new stdClass();
04853f27
TH
669 $quizstats->allattempts = $useallattempts;
670 $quizstats->firstattemptscount = $firstattempts->countrecs;
671 $quizstats->allattemptscount = $allattempts->countrecs;
672 $quizstats->firstattemptsavg = $firstattempts->average;
673 $quizstats->allattemptsavg = $allattempts->total / $allattempts->countrecs;
3b1d5cc4 674
04853f27
TH
675 // Recalculate sql again this time possibly including test for first attempt.
676 list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql(
677 $quizid, $currentgroup, $groupstudents, $useallattempts);
6f51ed72 678
768a7588 679 // Median ...
04853f27 680 if ($s % 2 == 0) {
768a7588 681 // An even number of attempts.
04853f27
TH
682 $limitoffset = $s/2 - 1;
683 $limit = 2;
684 } else {
685 $limitoffset = floor($s/2);
686 $limit = 1;
687 }
688 $sql = "SELECT id, sumgrades
689 FROM $fromqa
690 WHERE $whereqa
691 ORDER BY sumgrades";
3b1d5cc4 692
04853f27 693 $medianmarks = $DB->get_records_sql_menu($sql, $qaparams, $limitoffset, $limit);
3b1d5cc4 694
04853f27
TH
695 $quizstats->median = array_sum($medianmarks) / count($medianmarks);
696 if ($s > 1) {
768a7588
TH
697 // Fetch the sum of squared, cubed and power 4d
698 // differences between marks and mean mark.
04853f27 699 $mean = $usingattempts->total / $s;
0ff4bd08 700 $sql = "SELECT
25a03faa
TH
701 SUM(POWER((quiza.sumgrades - $mean), 2)) AS power2,
702 SUM(POWER((quiza.sumgrades - $mean), 3)) AS power3,
703 SUM(POWER((quiza.sumgrades - $mean), 4)) AS power4
04853f27
TH
704 FROM $fromqa
705 WHERE $whereqa";
706 $params = array('mean1' => $mean, 'mean2' => $mean, 'mean3' => $mean)+$qaparams;
3b1d5cc4 707
04853f27 708 $powers = $DB->get_record_sql($sql, $params, MUST_EXIST);
3b1d5cc4 709
768a7588 710 // Standard_Deviation:
728ebac7 711 // see http://docs.moodle.org/dev/Quiz_item_analysis_calculations_in_practise
768a7588 712 // #Standard_Deviation.
3b1d5cc4 713
04853f27
TH
714 $quizstats->standarddeviation = sqrt($powers->power2 / ($s - 1));
715
768a7588 716 // Skewness.
04853f27 717 if ($s > 2) {
768a7588
TH
718 // See http://docs.moodle.org/dev/
719 // Quiz_item_analysis_calculations_in_practise#Skewness_and_Kurtosis.
04853f27
TH
720 $m2= $powers->power2 / $s;
721 $m3= $powers->power3 / $s;
722 $m4= $powers->power4 / $s;
723
724 $k2= $s*$m2/($s-1);
725 $k3= $s*$s*$m3/(($s-1)*($s-2));
726 if ($k2) {
727 $quizstats->skewness = $k3 / (pow($k2, 3/2));
6f51ed72 728 }
e72efdd4 729 }
04853f27 730
768a7588 731 // Kurtosis.
04853f27
TH
732 if ($s > 3) {
733 $k4= $s*$s*((($s+1)*$m4)-(3*($s-1)*$m2*$m2))/(($s-1)*($s-2)*($s-3));
734 if ($k2) {
735 $quizstats->kurtosis = $k4 / ($k2*$k2);
43ec99aa 736 }
4f5ffac0 737 }
08a7ead5 738 }
04853f27
TH
739
740 $qstats = new quiz_statistics_question_stats($questions, $s, $summarksavg);
741 $qstats->load_step_data($quizid, $currentgroup, $groupstudents, $useallattempts);
742 $qstats->compute_statistics();
743
744 if ($s > 1) {
768a7588 745 $p = count($qstats->questions); // Number of positions.
04853f27 746 if ($p > 1 && isset($k2)) {
25a03faa
TH
747 $quizstats->cic = (100 * $p / ($p -1)) *
748 (1 - ($qstats->get_sum_of_mark_variance()) / $k2);
749 $quizstats->errorratio = 100 * sqrt(1 - ($quizstats->cic / 100));
750 $quizstats->standarderror = $quizstats->errorratio *
751 $quizstats->standarddeviation / 100;
04853f27
TH
752 }
753 }
754
755 return array($s, $quizstats, $qstats);
0c1c764e 756 }
3b1d5cc4 757
04853f27
TH
758 /**
759 * Load the cached statistics from the database.
760 *
761 * @param object $quiz the quiz settings
f7970e3c
TH
762 * @param int $currentgroup the current group. 0 for none.
763 * @param bool $nostudentsingroup true if there a no students.
764 * @param bool $useallattempts use all attempts, or just first attempts.
04853f27
TH
765 * @param array $groupstudents students in this group.
766 * @param array $questions question definitions.
767 * @return array with 4 elements:
768 * - $quizstats The statistics for overall attempt scores.
769 * - $questions The questions, with an additional _stats field.
770 * - $subquestions The subquestions, if any, with an additional _stats field.
771 * - $s Number of attempts included in the stats.
772 * If there is no cached data in the database, returns an array of four nulls.
773 */
774 protected function try_loading_cached_stats($quiz, $currentgroup,
775 $nostudentsingroup, $useallattempts, $groupstudents, $questions) {
43ec99aa 776 global $DB;
04853f27
TH
777
778 $timemodified = time() - self::TIME_TO_CACHE_STATS;
779 $quizstats = $DB->get_record_select('quiz_statistics',
780 'quizid = ? AND groupid = ? AND allattempts = ? AND timemodified > ?',
781 array($quiz->id, $currentgroup, $useallattempts, $timemodified));
782
783 if (!$quizstats) {
784 // No cached data found.
785 return array(null, $questions, null, null);
786 }
787
788 if ($useallattempts) {
789 $s = $quizstats->allattemptscount;
790 } else {
791 $s = $quizstats->firstattemptscount;
792 }
793
794 $subquestions = array();
795 $questionstats = $DB->get_records('quiz_question_statistics',
796 array('quizstatisticsid' => $quizstats->id));
797
798 $subquestionstats = array();
799 foreach ($questionstats as $stat) {
800 if ($stat->slot) {
801 $questions[$stat->slot]->_stats = $stat;
802 } else {
803 $subquestionstats[$stat->questionid] = $stat;
43ec99aa 804 }
04853f27
TH
805 }
806
807 if (!empty($subquestionstats)) {
808 $subqstofetch = array_keys($subquestionstats);
809 $subquestions = question_load_questions($subqstofetch);
810 foreach ($subquestions as $subqid => $subq) {
811 $subquestions[$subqid]->_stats = $subquestionstats[$subqid];
812 $subquestions[$subqid]->maxmark = $subq->defaultmark;
813 }
814 }
815
816 return array($quizstats, $questions, $subquestions, $s);
817 }
818
819 /**
820 * Store the statistics in the cache tables in the database.
821 *
822 * @param object $quizid the quiz id.
f7970e3c
TH
823 * @param int $currentgroup the current group. 0 for none.
824 * @param bool $useallattempts use all attempts, or just first attempts.
04853f27
TH
825 * @param object $quizstats The statistics for overall attempt scores.
826 * @param array $questions The questions, with an additional _stats field.
827 * @param array $subquestions The subquestions, if any, with an additional _stats field.
828 */
829 protected function cache_stats($quizid, $currentgroup,
830 $quizstats, $questions, $subquestions) {
831 global $DB;
832
833 $toinsert = clone($quizstats);
834 $toinsert->quizid = $quizid;
835 $toinsert->groupid = $currentgroup;
836 $toinsert->timemodified = time();
837
838 // Fix up some dodgy data.
839 if (isset($toinsert->errorratio) && is_nan($toinsert->errorratio)) {
840 $toinsert->errorratio = null;
841 }
842 if (isset($toinsert->standarderror) && is_nan($toinsert->standarderror)) {
843 $toinsert->standarderror = null;
844 }
845
846 // Store the data.
847 $quizstats->id = $DB->insert_record('quiz_statistics', $toinsert);
848
849 foreach ($questions as $question) {
850 $question->_stats->quizstatisticsid = $quizstats->id;
851 $DB->insert_record('quiz_question_statistics', $question->_stats, false);
852 }
853
854 foreach ($subquestions as $subquestion) {
855 $subquestion->_stats->quizstatisticsid = $quizstats->id;
856 $DB->insert_record('quiz_question_statistics', $subquestion->_stats, false);
857 }
858
859 return $quizstats->id;
860 }
861
862 /**
863 * Get the quiz and question statistics, either by loading the cached results,
864 * or by recomputing them.
865 *
866 * @param object $quiz the quiz settings.
f7970e3c
TH
867 * @param int $currentgroup the current group. 0 for none.
868 * @param bool $nostudentsingroup true if there a no students.
869 * @param bool $useallattempts use all attempts, or just first attempts.
04853f27
TH
870 * @param array $groupstudents students in this group.
871 * @param array $questions question definitions.
872 * @return array with 4 elements:
873 * - $quizstats The statistics for overall attempt scores.
874 * - $questions The questions, with an additional _stats field.
875 * - $subquestions The subquestions, if any, with an additional _stats field.
876 * - $s Number of attempts included in the stats.
877 */
878 protected function get_quiz_and_questions_stats($quiz, $currentgroup,
879 $nostudentsingroup, $useallattempts, $groupstudents, $questions) {
880
881 list($quizstats, $questions, $subquestions, $s) =
882 $this->try_loading_cached_stats($quiz, $currentgroup, $nostudentsingroup,
883 $useallattempts, $groupstudents, $questions);
884
885 if (is_null($quizstats)) {
886 list($s, $quizstats, $qstats) = $this->compute_stats($quiz->id,
887 $currentgroup, $nostudentsingroup, $useallattempts, $groupstudents, $questions);
888
889 if ($s) {
43ec99aa 890 $questions = $qstats->questions;
891 $subquestions = $qstats->subquestions;
04853f27
TH
892
893 $quizstatisticsid = $this->cache_stats($quiz->id, $currentgroup,
894 $quizstats, $questions, $subquestions);
895
896 $this->analyse_responses($quizstatisticsid, $quiz->id, $currentgroup,
897 $nostudentsingroup, $useallattempts, $groupstudents,
898 $questions, $subquestions);
43ec99aa 899 }
04853f27
TH
900 }
901
902 return array($quizstats, $questions, $subquestions, $s);
903 }
904
905 protected function analyse_responses($quizstatisticsid, $quizid, $currentgroup,
906 $nostudentsingroup, $useallattempts, $groupstudents, $questions, $subquestions) {
907
25a03faa
TH
908 $qubaids = quiz_statistics_qubaids_condition(
909 $quizid, $currentgroup, $groupstudents, $useallattempts);
04853f27
TH
910
911 $done = array();
912 foreach ($questions as $question) {
913 if (!question_bank::get_qtype($question->qtype, false)->can_analyse_responses()) {
914 continue;
43ec99aa 915 }
04853f27
TH
916 $done[$question->id] = 1;
917
918 $responesstats = new quiz_statistics_response_analyser($question);
919 $responesstats->analyse($qubaids);
920 $responesstats->store_cached($quizstatisticsid);
921 }
922
923 foreach ($subquestions as $question) {
924 if (!question_bank::get_qtype($question->qtype, false)->can_analyse_responses() ||
925 isset($done[$question->id])) {
926 continue;
43ec99aa 927 }
04853f27
TH
928 $done[$question->id] = 1;
929
930 $responesstats = new quiz_statistics_response_analyser($question);
931 $responesstats->analyse($qubaids);
932 $responesstats->store_cached($quizstatisticsid);
933 }
934 }
935
936 /**
937 * @return string HTML snipped for the Download full report as UI.
938 */
939 protected function everything_download_options() {
940 $downloadoptions = $this->table->get_download_menu();
941
0465ef6e
RT
942 $downloadelements = new stdClass();
943 $downloadelements->formatsmenu = html_writer::select($downloadoptions, 'download',
944 $this->table->defaultdownloadformat, false);
945 $downloadelements->downloadbutton = '<input type="submit" value="' .
946 get_string('download') . '"/>';
947
04853f27
TH
948 $output = '<form action="'. $this->table->baseurl .'" method="post">';
949 $output .= '<div class="mdl-align">';
950 $output .= '<input type="hidden" name="everything" value="1"/>';
0465ef6e 951 $output .= html_writer::tag('label', get_string('downloadeverything', 'quiz_statistics', $downloadelements));
04853f27
TH
952 $output .= '</div></form>';
953
954 return $output;
955 }
956
957 /**
958 * Generate the snipped of HTML that says when the stats were last caculated,
959 * with a recalcuate now button.
960 * @param object $quizstats the overall quiz statistics.
f7970e3c
TH
961 * @param int $quizid the quiz id.
962 * @param int $currentgroup the id of the currently selected group, or 0.
04853f27 963 * @param array $groupstudents ids of students in the group.
25a03faa
TH
964 * @param bool $useallattempts whether to use all attempts, instead of just
965 * first attempts.
966 * @return string a HTML snipped saying when the stats were last computed,
967 * or blank if that is not appropriate.
04853f27
TH
968 */
969 protected function output_caching_info($quizstats, $quizid, $currentgroup,
970 $groupstudents, $useallattempts, $reporturl) {
971 global $DB, $OUTPUT;
972
973 if (empty($quizstats->timemodified)) {
974 return '';
975 }
976
977 // Find the number of attempts since the cached statistics were computed.
978 list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql(
979 $quizid, $currentgroup, $groupstudents, $useallattempts, true);
980 $count = $DB->count_records_sql("
981 SELECT COUNT(1)
982 FROM $fromqa
983 WHERE $whereqa
984 AND quiza.timefinish > {$quizstats->timemodified}", $qaparams);
985
986 if (!$count) {
987 $count = 0;
988 }
989
990 // Generate the output.
0ff4bd08 991 $a = new stdClass();
04853f27
TH
992 $a->lastcalculated = format_time(time() - $quizstats->timemodified);
993 $a->count = $count;
994
25a03faa
TH
995 $recalcualteurl = new moodle_url($reporturl,
996 array('recalculate' => 1, 'sesskey' => sesskey()));
04853f27 997 $output = '';
25a03faa
TH
998 $output .= $OUTPUT->box_start(
999 'boxaligncenter generalbox boxwidthnormal mdl-align', 'cachingnotice');
04853f27 1000 $output .= get_string('lastcalculated', 'quiz_statistics', $a);
25a03faa
TH
1001 $output .= $OUTPUT->single_button($recalcualteurl,
1002 get_string('recalculatenow', 'quiz_statistics'));
04853f27
TH
1003 $output .= $OUTPUT->box_end(true);
1004
1005 return $output;
1006 }
1007
1008 /**
1009 * Clear the cached data for a particular report configuration. This will
1010 * trigger a re-computation the next time the report is displayed.
f7970e3c
TH
1011 * @param int $quizid the quiz id.
1012 * @param int $currentgroup a group id, or 0.
1013 * @param bool $useallattempts whether all attempts, or just first attempts are included.
04853f27
TH
1014 */
1015 protected function clear_cached_data($quizid, $currentgroup, $useallattempts) {
1016 global $DB;
1017
1018 $todelete = $DB->get_records_menu('quiz_statistics', array('quizid' => $quizid,
25a03faa 1019 'groupid' => $currentgroup, 'allattempts' => $useallattempts), '', 'id, 1');
04853f27
TH
1020
1021 if (!$todelete) {
1022 return;
1023 }
1024
1025 list($todeletesql, $todeleteparams) = $DB->get_in_or_equal(array_keys($todelete));
1026
25a03faa
TH
1027 $DB->delete_records_select('quiz_question_statistics',
1028 'quizstatisticsid ' . $todeletesql, $todeleteparams);
1029 $DB->delete_records_select('quiz_question_response_stats',
1030 'quizstatisticsid ' . $todeletesql, $todeleteparams);
1031 $DB->delete_records_select('quiz_statistics',
1032 'id ' . $todeletesql, $todeleteparams);
04853f27
TH
1033 }
1034
1035 /**
f7970e3c 1036 * @param bool $useallattempts whether we are using all attempts.
04853f27
TH
1037 * @return the appropriate lang string to describe this option.
1038 */
1039 protected function using_attempts_string($useallattempts) {
1040 if ($useallattempts) {
1041 return get_string('allattempts', 'quiz_statistics');
1042 } else {
1043 return get_string('firstattempts', 'quiz_statistics');
43ec99aa 1044 }
43ec99aa 1045 }
3652dddd
JP
1046
1047 /**
1048 * @param object $quiz the quiz.
1049 * @return array of questions for this quiz.
1050 */
1051 public function load_and_initialise_questions_for_calculations($quiz) {
1052 // Load the questions.
1053 $questions = quiz_report_get_significant_questions($quiz);
1054 $questionids = array();
1055 foreach ($questions as $question) {
1056 $questionids[] = $question->id;
1057 }
1058 $fullquestions = question_load_questions($questionids);
1059 foreach ($questions as $qno => $question) {
1060 $q = $fullquestions[$question->id];
1061 $q->maxmark = $question->maxmark;
1062 $q->slot = $qno;
1063 $q->number = $question->number;
1064 $questions[$qno] = $q;
1065 }
1066 return $questions;
1067 }
0c1c764e 1068}
04853f27
TH
1069
1070function quiz_statistics_attempts_sql($quizid, $currentgroup, $groupstudents,
1071 $allattempts = true, $includeungraded = false) {
9a160c7c 1072 global $DB;
04853f27
TH
1073
1074 $fromqa = '{quiz_attempts} quiza ';
1075
2cdd2df9
TH
1076 $whereqa = 'quiza.quiz = :quizid AND quiza.preview = 0 AND quiza.state = :quizstatefinished';
1077 $qaparams = array('quizid' => $quizid, 'quizstatefinished' => quiz_attempt::FINISHED);
04853f27 1078
71a2b878 1079 if (!empty($currentgroup) && $groupstudents) {
b149b788 1080 list($grpsql, $grpparams) = $DB->get_in_or_equal(array_keys($groupstudents),
a2ac2349 1081 SQL_PARAMS_NAMED, 'u');
04853f27 1082 $whereqa .= " AND quiza.userid $grpsql";
71a2b878 1083 $qaparams += $grpparams;
1084 }
04853f27
TH
1085
1086 if (!$allattempts) {
1087 $whereqa .= ' AND quiza.attempt = 1';
1088 }
1089
1090 if (!$includeungraded) {
1091 $whereqa .= ' AND quiza.sumgrades IS NOT NULL';
71a2b878 1092 }
04853f27 1093
71a2b878 1094 return array($fromqa, $whereqa, $qaparams);
1095}
869309b8 1096
04853f27
TH
1097/**
1098 * Return a {@link qubaid_condition} from the values returned by
1099 * {@link quiz_statistics_attempts_sql}
1100 * @param string $fromqa from quiz_statistics_attempts_sql.
1101 * @param string $whereqa from quiz_statistics_attempts_sql.
1102 */
1103function quiz_statistics_qubaids_condition($quizid, $currentgroup, $groupstudents,
1104 $allattempts = true, $includeungraded = false) {
1105 list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql($quizid, $currentgroup,
1106 $groupstudents, $allattempts, $includeungraded);
1107 return new qubaid_join($fromqa, 'quiza.uniqueid', $whereqa, $qaparams);
1108}