3 * This script lists student attempts
6 * @author Martin Dougiamas, Tim Hunt and others.
7 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
11 require_once($CFG->libdir.'/tablelib.php');
13 class quiz_report extends quiz_default_report {
18 function display($quiz, $cm, $course) {
21 // Define some strings
22 $strreallydel = addslashes(get_string('deleteattemptcheck','quiz'));
23 $strtimeformat = get_string('strftimedatetime');
24 $strreviewquestion = get_string('reviewresponse', 'quiz');
26 $context = get_context_instance(CONTEXT_MODULE, $cm->id);
28 // Only print headers if not asked to download data
29 if (!$download = optional_param('download', NULL)) {
30 $this->print_header_and_tabs($cm, $course, $quiz, $reportmode="overview");
34 $action = optional_param('action', '', PARAM_ACTION);
37 case 'delete': // Some attempts need to be deleted
38 require_capability('mod/quiz:deleteattempts', $context);
39 $attemptids = optional_param('attemptid', array(), PARAM_INT);
41 foreach($attemptids as $attemptid) {
42 add_to_log($course->id, 'quiz', 'delete attempt', 'report.php?id=' . $cm->id,
44 quiz_delete_attempt($attemptid, $quiz);
49 // Set of format options for teacher-created content, for example overall feedback.
50 $nocleanformatoptions = new stdClass;
51 $nocleanformatoptions->noclean = true;
53 // Prepare list of available actions to perform on attempts - we only want to show the checkbox.
54 // Column on the table if there are options.
55 $attemptactions = array();
56 if (has_capability('mod/quiz:deleteattempts', $context)) {
57 $attemptactions['delete'] = get_string('delete');
60 // Work out some display options - whether there is feedback, and whether scores should be shown.
61 $hasfeedback = quiz_has_feedback($quiz->id) && $quiz->grade > 1.e-7 && $quiz->sumgrades > 1.e-7;
62 $fakeattempt = new stdClass();
63 $fakeattempt->preview = false;
64 $fakeattempt->timefinish = $quiz->timeopen;
65 $reviewoptions = quiz_get_reviewoptions($quiz, $fakeattempt, $context);
66 $showgrades = $quiz->grade && $quiz->sumgrades && $reviewoptions->scores;
69 $noattempts = optional_param('noattempts', 0, PARAM_INT);
70 $detailedmarks = optional_param('detailedmarks', 0, PARAM_INT);
71 $pagesize = optional_param('pagesize', 0, PARAM_INT);
72 $reporturl = $CFG->wwwroot.'/mod/quiz/report.php?mode=overview';
74 $pagesize = QUIZ_REPORT_DEFAULT_PAGE_SIZE;
76 if (!$reviewoptions->scores) {
79 $reporturlwithoptions = $reporturl . '&id=' . $cm->id . '&noattempts=' . $noattempts .
80 '&detailedmarks=' . $detailedmarks . '&pagesize=' . $pagesize;
82 /// find out current groups mode
83 $currentgroup = groups_get_activity_group($cm, true);
85 if ($groupmode = groups_get_activity_groupmode($cm)) { // Groups are being used
87 groups_print_activity_menu($cm, $reporturlwithoptions);
91 // Print information on the number of existing attempts
92 if (!$download) { //do not print notices when downloading
93 if ($strattemptnum = quiz_num_attempt_summary($quiz, $cm, false, $currentgroup)) {
94 echo '<div class="quizattemptcounts">' . $strattemptnum . '</div>';
98 // Now check if asked download of data
100 $filename = clean_filename("$course->shortname ".format_string($quiz->name,true));
104 // Define table columns
105 $tablecolumns = array('picture', 'fullname', 'timestart', 'timefinish', 'duration');
106 $tableheaders = array('', get_string('name'), get_string('startedon', 'quiz'),
107 get_string('timecompleted','quiz'), get_string('attemptduration', 'quiz'));
109 if (!empty($attemptactions)) {
110 array_unshift($tablecolumns, 'checkbox');
111 array_unshift($tableheaders, NULL);
115 $tablecolumns[] = 'sumgrades';
116 $tableheaders[] = get_string('grade', 'quiz').'/'.$quiz->grade;
119 if ($detailedmarks) {
120 // we want to display marks for all questions
121 // Start by getting all questions
122 $questionlist = quiz_questions_in_quiz($quiz->questions);
123 $questionids = explode(',', $questionlist);
124 $sql = "SELECT q.*, i.grade AS maxgrade, i.id AS instance".
125 " FROM {$CFG->prefix}question q,".
126 " {$CFG->prefix}quiz_question_instances i".
127 " WHERE i.quiz = '$quiz->id' AND q.id = i.question".
128 " AND q.id IN ($questionlist)";
129 if (!$questions = get_records_sql($sql)) {
130 print_error('No questions found');
133 foreach ($questionids as $key => $id) {
134 if ($questions[$id]->length) {
135 // Only print questions of non-zero length
136 $tablecolumns[] = '$'.$id;
137 $tableheaders[] = '#'.$number;
138 $questions[$id]->number = $number;
139 $number += $questions[$id]->length;
141 // get rid of zero length questions
142 unset($questions[$id]);
143 unset($questionids[$key]);
149 $tablecolumns[] = 'feedbacktext';
150 $tableheaders[] = get_string('feedback', 'quiz');
156 $table = new flexible_table('mod-quiz-report-overview-report');
158 $table->define_columns($tablecolumns);
159 $table->define_headers($tableheaders);
160 $table->define_baseurl($reporturlwithoptions);
162 $table->sortable(true);
163 $table->collapsible(true);
165 $table->column_suppress('picture');
166 $table->column_suppress('fullname');
168 $table->column_class('picture', 'picture');
170 $table->set_attribute('cellspacing', '0');
171 $table->set_attribute('id', 'attempts');
172 $table->set_attribute('class', 'generaltable generalbox');
174 // Start working -- this is necessary as soon as the niceties are over
176 } else if ($download =='ODS') {
177 require_once("$CFG->libdir/odslib.class.php");
180 // Creating a workbook
181 $workbook = new MoodleODSWorkbook("-");
182 // Sending HTTP headers
183 $workbook->send($filename);
184 // Creating the first worksheet
185 $sheettitle = get_string('reportoverview','quiz');
186 $myxls =& $workbook->add_worksheet($sheettitle);
188 $format =& $workbook->add_format();
189 $format->set_bold(0);
190 $formatbc =& $workbook->add_format();
191 $formatbc->set_bold(1);
192 $formatbc->set_align('center');
193 $formatb =& $workbook->add_format();
194 $formatb->set_bold(1);
195 $formaty =& $workbook->add_format();
196 $formaty->set_bg_color('yellow');
197 $formatc =& $workbook->add_format();
198 $formatc->set_align('center');
199 $formatr =& $workbook->add_format();
200 $formatr->set_bold(1);
201 $formatr->set_color('red');
202 $formatr->set_align('center');
203 $formatg =& $workbook->add_format();
204 $formatg->set_bold(1);
205 $formatg->set_color('green');
206 $formatg->set_align('center');
207 // Here starts workshhet headers
209 $headers = array(get_string('name'), get_string('startedon', 'quiz'),
210 get_string('timecompleted', 'quiz'), get_string('attemptduration', 'quiz'));
213 $headers[] = get_string('grade', 'quiz').'/'.$quiz->grade;
216 foreach ($questionids as $id) {
217 $headers[] = '#'.$questions[$id]->number;
221 $headers[] = get_string('feedback', 'quiz');
224 foreach ($headers as $item) {
225 $myxls->write(0,$colnum,$item,$formatbc);
229 } else if ($download =='Excel') {
230 require_once("$CFG->libdir/excellib.class.php");
233 // Creating a workbook
234 $workbook = new MoodleExcelWorkbook("-");
235 // Sending HTTP headers
236 $workbook->send($filename);
237 // Creating the first worksheet
238 $sheettitle = get_string('reportoverview','quiz');
239 $myxls =& $workbook->add_worksheet($sheettitle);
241 $format =& $workbook->add_format();
242 $format->set_bold(0);
243 $formatbc =& $workbook->add_format();
244 $formatbc->set_bold(1);
245 $formatbc->set_align('center');
246 $formatb =& $workbook->add_format();
247 $formatb->set_bold(1);
248 $formaty =& $workbook->add_format();
249 $formaty->set_bg_color('yellow');
250 $formatc =& $workbook->add_format();
251 $formatc->set_align('center');
252 $formatr =& $workbook->add_format();
253 $formatr->set_bold(1);
254 $formatr->set_color('red');
255 $formatr->set_align('center');
256 $formatg =& $workbook->add_format();
257 $formatg->set_bold(1);
258 $formatg->set_color('green');
259 $formatg->set_align('center');
260 // Here starts workshhet headers
262 $headers = array(get_string('name'), get_string('startedon', 'quiz'),
263 get_string('timecompleted', 'quiz'), get_string('attemptduration', 'quiz'));
266 $headers[] = get_string('grade', 'quiz').'/'.$quiz->grade;
269 foreach ($questionids as $id) {
270 $headers[] = '#'.$questions[$id]->number;
274 $headers[] = get_string('feedback', 'quiz');
277 foreach ($headers as $item) {
278 $myxls->write(0,$colnum,$item,$formatbc);
282 } else if ($download=='CSV') {
285 header("Content-Type: application/download\n");
286 header("Content-Disposition: attachment; filename=\"$filename\"");
287 header("Expires: 0");
288 header("Cache-Control: must-revalidate,post-check=0,pre-check=0");
289 header("Pragma: public");
291 $headers = get_string('name')."\t".get_string('startedon', 'quiz')."\t".
292 get_string('timecompleted', 'quiz')."\t".get_string('attemptduration', 'quiz');
295 $headers .= "\t".get_string('grade', 'quiz')."/".$quiz->grade;
298 foreach ($questionids as $id) {
299 $headers .= "\t#".$questions[$id]->number;
303 $headers .= "\t" . get_string('feedback', 'quiz');
308 // Get users with quiz attempt capability 'students'.
309 // don't need to do this expensive call if we are listing all attempts though.
310 if ( $noattempts != 3 ) {
311 if (empty($currentgroup)) {
313 // all users who can attempt quizzes
314 $allowed = join(',',array_keys(get_users_by_capability($context, 'mod/quiz:attempt','','','','','','',false)));
317 // all users who can attempt quizzes and who are in the currently selected group
318 $allowed = join(',',array_keys(get_users_by_capability($context, 'mod/quiz:attempt','','','','',$currentgroup,'',false)));
323 $select = 'SELECT '.sql_concat('u.id', '\'#\'', $db->IfNull('qa.attempt', '0')).' AS uniqueid, '.
324 'qa.uniqueid AS attemptuniqueid, qa.id AS attempt, u.id AS userid, u.firstname, u.lastname, u.picture, '.
325 'qa.sumgrades, qa.timefinish, qa.timestart, qa.timefinish - qa.timestart AS duration ';
327 // This part is the same for all cases - join users and quiz_attempts tables
328 $from = 'FROM '.$CFG->prefix.'user u ';
329 $from .= 'LEFT JOIN '.$CFG->prefix.'quiz_attempts qa ON qa.userid = u.id AND qa.quiz = '.$quiz->id;
331 if ( $noattempts == 3 ) { // Show all attempts, including students who are no longer in the course
333 $where = ' WHERE qa.id IS NOT NULL';
334 // Comment out the following line to include preview attempts in the 'show all attempts' filter
335 $where .= ' AND qa.preview = 0';
336 } else { // All non-admin users with quiz attempt capabilites - e.g. students
338 $where = ' WHERE u.id IN (' .$allowed. ') AND (qa.preview = 0 OR qa.preview IS NULL)';
340 if ( empty( $noattempts )) { // Show only students with attempts
341 $where .= ' AND qa.id IS NOT NULL';
342 } else if ( $noattempts == 1 ) { // Show only students without attempts
343 $where .= ' AND qa.id IS NULL';
347 $countsql = 'SELECT COUNT(DISTINCT('.sql_concat('u.id', '\'#\'', $db->IfNull('qa.attempt', '0')).')) '.$from.$where;
350 // Add extra limits due to initials bar
351 if($table->get_sql_where()) {
352 $where .= ' AND '.$table->get_sql_where();
355 // Count the records NOW, before funky question grade sorting messes up $from
356 if (!empty($countsql)) {
357 $totalinitials = count_records_sql($countsql);
358 if ($table->get_sql_where()) {
359 $countsql .= ' AND '.$table->get_sql_where();
361 $total = count_records_sql($countsql);
365 // Add extra limits due to sorting by question grade
366 if($sort = $table->get_sql_sort()) {
367 $sortparts = explode(',', $sort);
369 $questionsort = false;
370 foreach($sortparts as $sortpart) {
371 $sortpart = trim($sortpart);
372 if(substr($sortpart, 0, 1) == '$') {
374 $qid = intval(substr($sortpart, 1));
375 $select .= ', grade ';
376 $from .= ' LEFT JOIN '.$CFG->prefix.'question_sessions qns ON qns.attemptid = qa.id '.
377 'LEFT JOIN '.$CFG->prefix.'question_states qs ON qs.id = qns.newgraded ';
378 $where .= ' AND (qns.questionid IS NULL OR qns.questionid = '.$qid.')';
379 $newsort[] = 'grade '.(strpos($sortpart, 'ASC')? 'ASC' : 'DESC');
380 $questionsort = true;
383 $newsort[] = $sortpart;
387 // Reconstruct the sort string
388 $sort = ' ORDER BY '.implode(', ', $newsort);
391 // Fix some wired sorting
393 $sort = ' ORDER BY uniqueid';
396 $table->pagesize($pagesize, $total);
399 // If there is feedback, include it in the query.
401 $factor = $quiz->grade/$quiz->sumgrades;
402 $select .= ', qf.feedbacktext ';
403 $from .= " LEFT JOIN {$CFG->prefix}quiz_feedback qf ON " .
404 "qf.quizid = $quiz->id AND qf.mingrade <= qa.sumgrades * $factor AND qa.sumgrades * $factor < qf.maxgrade";
407 // Fetch the attempts
408 if (!empty($from)) { // if we're in the site course and displaying no attempts, it makes no sense to do the query.
410 $attempts = get_records_sql($select.$from.$where.$sort,
411 $table->get_page_start(), $table->get_page_size());
413 $attempts = get_records_sql($select.$from.$where.$sort);
421 $table->initialbars($totalinitials>20);
424 if(!empty($attempts) || !empty($noattempts)) {
426 foreach ($attempts as $attempt) {
428 $picture = print_user_picture($attempt->userid, $course->id, $attempt->picture, false, true);
430 $userlink = '<a href="'.$CFG->wwwroot.'/user/view.php?id='.$attempt->userid.
431 '&course='.$course->id.'">'.fullname($attempt).'</a>';
436 if (!empty($attemptactions)) {
437 $row[] = '<input type="checkbox" name="attemptid[]" value="'.$attempt->attempt.'" />';
442 $row[] = fullname($attempt);
446 if ($attempt->attempt) {
447 $startdate = userdate($attempt->timestart, $strtimeformat);
449 $row[] = '<a href="review.php?q='.$quiz->id.'&attempt='.$attempt->attempt.'">'.$startdate.'</a>';
453 if ($attempt->timefinish) {
454 $timefinish = userdate($attempt->timefinish, $strtimeformat);
455 $duration = format_time($attempt->duration);
457 $row[] = '<a href="review.php?q='.$quiz->id.'&attempt='.$attempt->attempt.'">'.$timefinish.'</a>';
459 $row[] = $timefinish;
464 $row[] = get_string('unfinished', 'quiz');
474 if ($attempt->timefinish) {
475 $grade = quiz_rescale_grade($attempt->sumgrades, $quiz);
477 $row[] = '<a href="review.php?q='.$quiz->id.'&attempt='.$attempt->attempt.'">'.$grade.'</a>';
487 if(empty($attempt->attempt)) {
488 foreach($questionids as $questionid) {
492 foreach($questionids as $questionid) {
493 $gradedstateid = get_field('question_sessions', 'newgraded', 'attemptid',
494 $attempt->attemptuniqueid, 'questionid', $questionid);
495 if ($gradedstateid) {
496 $grade = round(get_field('question_states', 'grade', 'id',
497 $gradedstateid), $quiz->decimalpoints);
502 $row[] = link_to_popup_window('/mod/quiz/reviewquestion.php?state='.
503 $gradedstateid.'&number='.$questions[$questionid]->number,
504 'reviewquestion', $grade, 450, 650, $strreviewquestion, 'none', true);
514 if ($attempt->timefinish) {
515 $row[] = format_text($attempt->feedbacktext, FORMAT_MOODLE, $nocleanformatoptions);
521 $table->add_data($row);
522 } else if ($download == 'Excel' or $download == 'ODS') {
524 foreach($row as $item){
525 $myxls->write($rownum,$colnum,$item,$format);
529 } else if ($download=='CSV') {
530 $text = implode("\t", $row);
537 echo '<div id="tablecontainer">';
538 echo '<form id="attemptsform" method="post" action="' . $reporturlwithoptions .
539 '" onsubmit="var menu = document.getElementById(\'menuaction\'); ' .
540 'return (menu.options[menu.selectedIndex].value == \'delete\' ? confirm(\''.
541 $strreallydel.'\') : true);">';
545 $table->print_html();
547 // Print "Select all" etc.
548 if (!empty($attempts) && !empty($attemptactions)) {
549 echo '<table id="commands">';
551 echo '<a href="javascript:select_all_in(\'DIV\',null,\'tablecontainer\');">'.
552 get_string('selectall', 'quiz').'</a> / ';
553 echo '<a href="javascript:deselect_all_in(\'DIV\',null,\'tablecontainer\');">'.
554 get_string('selectnone', 'quiz').'</a> ';
556 choose_from_menu($attemptactions, 'action', '', get_string('withselected', 'quiz'),
557 'if(this.selectedIndex > 0) submitFormById(\'attemptsform\');');
558 echo '<noscript id="noscriptmenuaction" style="display: inline;"><div>';
559 echo '<input type="submit" value="'.get_string('go').'" /></div></noscript>';
560 echo '<script type="text/javascript">
562 document.getElementById("noscriptmenuaction").style.display = "none";
565 echo '</td></tr></table>';
569 echo '</form></div>';
571 if (!empty($attempts)) {
572 echo '<table class="boxaligncenter"><tr>';
574 $options["id"] = $cm->id;
575 $options["q"] = $quiz->id;
576 $options['sesskey'] = sesskey();
577 $options["noheader"] = "yes";
578 $options['noattempts'] = $noattempts;
579 $options['detailedmarks'] = $detailedmarks;
581 $options["download"] = "ODS";
582 print_single_button($reporturl, $options, get_string("downloadods"));
585 $options["download"] = "Excel";
586 print_single_button($reporturl, $options, get_string("downloadexcel"));
589 $options["download"] = "CSV";
590 print_single_button($reporturl, $options, get_string("downloadtext"));
593 helpbutton('overviewdownload', get_string('overviewdownload', 'quiz_overview'), 'quiz');
595 echo '</tr></table>';
597 } else if ($download == 'Excel' or $download == 'ODS') {
600 } else if ($download == 'CSV') {
606 $table->print_html();
609 // Print display options
610 echo '<div class="controls">';
611 echo '<form id="options" action="' . $reporturl . '" method="get">';
613 echo '<p>'.get_string('displayoptions', 'quiz').': </p>';
614 echo '<input type="hidden" name="id" value="'.$cm->id.'" />';
615 echo '<input type="hidden" name="q" value="'.$quiz->id.'" />';
616 echo '<input type="hidden" name="noattempts" value="0" />';
617 echo '<input type="hidden" name="detailedmarks" value="0" />';
618 echo '<table id="overview-options" class="boxaligncenter">';
619 echo '<tr align="left">';
620 echo '<td><label for="pagesize">'.get_string('pagesize', 'quiz').'</label></td>';
621 echo '<td><input type="text" id="pagesize" name="pagesize" size="3" value="'.$pagesize.'" /></td>';
623 echo '<tr align="left">';
624 echo '<td colspan="2">';
625 $options = array(0 => get_string('attemptsonly','quiz_overview', $course->students));
626 if ($course->id != SITEID) {
627 $options[1] = get_string('noattemptsonly', 'quiz_overview', $course->students);
628 $options[2] = get_string('allstudents','quiz_overview', $course->students);
629 $options[3] = get_string('allattempts','quiz_overview');
631 choose_from_menu($options,'noattempts',$noattempts,'');
633 echo '<tr align="left">';
634 echo '<td colspan="2">';
635 echo '<input type="checkbox" id="checkdetailedmarks" name="detailedmarks" '.
636 ($detailedmarks?'checked="checked" ':'').'value="1" /> ';
637 echo '<label for="checkdetailedmarks">'.get_string('showdetailedmarks', 'quiz').'</label> ';
639 echo '<tr><td colspan="2" align="center">';
640 echo '<input type="submit" value="'.get_string('go').'" />';
641 echo '</td></tr></table>';