MDL-13678 "Change default number of rows per page on quiz reports" Made a new constan...
[moodle.git] / mod / quiz / report / grading / report.php
CommitLineData
ec65c6ad 1<?php // $Id$
bc67d9b3 2/**
3 * Quiz report to help teachers manually grade quiz questions that need it.
4 *
5 * @package quiz
6 * @subpackage reports
7 */
8
9// Flow of the file:
10// Get variables, run essential queries
11// Check for post data submitted. If exists, then process data (the data is the grades and comments for essay questions)
12// Check for userid, attemptid, or gradeall and for questionid. If found, print out the appropriate essay question attempts
13// Switch:
14// first case: print out all essay questions in quiz and the number of ungraded attempts
15// second case: print out all users and their attempts for a specific essay question
16
17require_once($CFG->dirroot . "/mod/quiz/editlib.php");
18require_once($CFG->libdir . '/tablelib.php');
19
20/**
21 * Quiz report to help teachers manually grade quiz questions that need it.
22 *
23 * @package quiz
24 * @subpackage reports
25 */
31e95855 26class quiz_report extends quiz_default_report {
bc67d9b3 27 /**
28 * Displays the report.
29 */
30 function display($quiz, $cm, $course) {
80bbc7d7 31
31e95855 32 $action = optional_param('action', 'viewquestions', PARAM_ALPHA);
33 $questionid = optional_param('questionid', 0, PARAM_INT);
31e95855 34
35 $this->print_header_and_tabs($cm, $course, $quiz, $reportmode="grading");
9d4cbe56 36
f4850b7e 37 // Check permissions
38 $context = get_context_instance(CONTEXT_MODULE, $cm->id);
39 if (!has_capability('mod/quiz:grade', $context)) {
40 notify(get_string('gradingnotallowed', 'quiz_grading'));
41 return true;
42 }
43
31e95855 44 if (!empty($questionid)) {
45 if (! $question = get_record('question', 'id', $questionid)) {
5a2a5331 46 print_error("Question with id $questionid not found");
31e95855 47 }
80bbc7d7 48 $question->maxgrade = get_field('quiz_question_instances', 'grade', 'quiz', $quiz->id, 'question', $question->id);
49
50 // Some of the questions code is optimised to work with several questions
51 // at once so it wants the question to be in an array. The array key
52 // must be the question id.
53 $key = $question->id;
54 $questions[$key] = &$question;
55
56 // We need to add additional questiontype specific information to
57 // the question objects.
58 if (!get_question_options($questions)) {
5a2a5331 59 print_error("Unable to load questiontype specific question information");
80bbc7d7 60 }
61 // This will have extended the question object so that it now holds
62 // all the information about the questions that may be needed later.
31e95855 63 }
80bbc7d7 64
31e95855 65 add_to_log($course->id, "quiz", "manualgrading", "report.php?mode=grading&amp;q=$quiz->id", "$quiz->id", "$cm->id");
80bbc7d7 66
31e95855 67 echo '<div id="overDiv" style="position:absolute; visibility:hidden; z-index:1000;"></div>'; // for overlib
80bbc7d7 68
31e95855 69 if ($data = data_submitted()) { // post data submitted, process it
70 confirm_sesskey();
80bbc7d7 71
e99b3759 72 // now go through all of the responses and save them.
73 foreach($data->manualgrades as $uniqueid => $response) {
31e95855 74 // get our attempt
e99b3759 75 if (! $attempt = get_record('quiz_attempts', 'uniqueid', $uniqueid)) {
5a2a5331 76 print_error('No such attempt ID exists');
80bbc7d7 77 }
78
79 // Load the state for this attempt (The questions array was created earlier)
80 $states = get_question_states($questions, $quiz, $attempt);
81 // The $states array is indexed by question id but because we are dealing
82 // with only one question there is only one entry in this array
83 $state = &$states[$question->id];
84
b6e907a2 85 // the following will update the state and attempt
86 question_process_comment($question, $state, $attempt, $response['comment'], $response['grade']);
80bbc7d7 87
b6e907a2 88 // If the state has changed save it and update the quiz grade
89 if ($state->changed) {
90 save_question_session($question, $state);
e99b3759 91 quiz_save_best_grade($quiz, $attempt->userid);
b6e907a2 92 }
31e95855 93 }
fca490bc 94 notify(get_string('changessaved', 'quiz'), 'notifysuccess');
31e95855 95 }
80bbc7d7 96
e99b3759 97 // our 3 different views
98 // the first one displays all of the manually graded questions in the quiz
99 // with the number of ungraded attempts for each question
80bbc7d7 100
101 // the second view displays the users who have answered the essay question
31e95855 102 // and all of their attempts at answering the question
9d4cbe56 103
e99b3759 104 // the third prints the question with a comment
105 // and grade form underneath it
9d4cbe56 106
31e95855 107 switch($action) {
108 case 'viewquestions':
e99b3759 109 $this->view_questions($quiz);
31e95855 110 break;
111 case 'viewquestion':
e99b3759 112 $this->view_question($quiz, $question);
113 break;
114 case 'grade':
115 $this->print_questions_and_form($quiz, $question);
31e95855 116 break;
31e95855 117 }
118 return true;
119 }
9d4cbe56 120
0a525211 121 /**
122 * Prints a table containing all manually graded questions
123 *
124 * @param object $quiz Quiz object of the currrent quiz
125 * @param object $course Course object of the current course
126 * @param string $userids Comma-separated list of userids in this course
e99b3759 127 * @return boolean
0a525211 128 * @todo Look for the TODO in this code to see what still needs to be done
129 **/
e99b3759 130 function view_questions($quiz) {
9d4cbe56 131 global $CFG, $QTYPE_MANUAL;
132
e99b3759 133 $users = get_course_students($quiz->course);
134
135 if(empty($users)) {
136 print_heading(get_string("noattempts", "quiz"));
137 return true;
138 }
9d4cbe56 139
0a525211 140 // setup the table
141 $table = new stdClass;
142 $table->head = array(get_string("essayquestions", "quiz"), get_string("ungraded", "quiz"));
143 $table->align = array("left", "left");
144 $table->wrap = array("wrap", "wrap");
145 $table->width = "20%";
146 $table->size = array("*", "*");
147 $table->data = array();
148
149 // get the essay questions
150 $questionlist = quiz_questions_in_quiz($quiz->questions);
151 $sql = "SELECT q.*, i.grade AS maxgrade, i.id AS instance".
152 " FROM {$CFG->prefix}question q,".
153 " {$CFG->prefix}quiz_question_instances i".
154 " WHERE i.quiz = '$quiz->id' AND q.id = i.question".
155 " AND q.id IN ($questionlist)".
9d4cbe56 156 " AND q.qtype IN ($QTYPE_MANUAL)".
0a525211 157 " ORDER BY q.name";
158 if (empty($questionlist) or !$questions = get_records_sql($sql)) {
0a525211 159 print_heading(get_string('noessayquestionsfound', 'quiz'));
e99b3759 160 return false;
0a525211 161 }
9d4cbe56 162
163 notify(get_string('essayonly', 'quiz_grading'));
164
0a525211 165 // get all the finished attempts by the users
e99b3759 166 $userids = implode(', ', array_keys($users));
0a525211 167 if ($attempts = get_records_select('quiz_attempts', "quiz = $quiz->id and timefinish > 0 AND userid IN ($userids) AND preview = 0", 'userid, attempt')) {
168 foreach($questions as $question) {
169
170 $link = "<a href=\"report.php?mode=grading&amp;q=$quiz->id&amp;action=viewquestion&amp;questionid=$question->id\">".
171 $question->name."</a>";
172 // determine the number of ungraded attempts
173 $ungraded = 0;
174 foreach ($attempts as $attempt) {
e99b3759 175 if (!$this->is_graded($question, $attempt)) {
0a525211 176 $ungraded++;
177 }
178 }
179
180 $table->data[] = array($link, $ungraded);
181 }
182 print_table($table);
183 } else {
184 print_heading(get_string('noattempts', 'quiz'));
185 }
9d4cbe56 186
e99b3759 187 return true;
0a525211 188 }
9d4cbe56 189
fbe4d5ce 190 /**
191 * Prints a table with users and their attempts
192 *
193 * @return void
194 * @todo Add current grade to the table
195 * Finnish documenting
196 **/
e99b3759 197 function view_question($quiz, $question) {
fbe4d5ce 198 global $CFG, $db;
9d4cbe56 199
e99b3759 200 $users = get_course_students($quiz->course);
201 $userids = implode(',', array_keys($users));
202 $usercount = count($users);
9d4cbe56 203
e99b3759 204 // set up table
bc67d9b3 205 $tablecolumns = array('picture', 'fullname', 'timefinish', 'grade');
260812ba 206 $tableheaders = array('', get_string('name'), get_string("completedon", "quiz"), '');
fbe4d5ce 207
208 $table = new flexible_table('mod-quiz-report-grading');
209
210 $table->define_columns($tablecolumns);
211 $table->define_headers($tableheaders);
212 $table->define_baseurl($CFG->wwwroot.'/mod/quiz/report.php?mode=grading&amp;q='.$quiz->id.'&amp;action=viewquestion&amp;questionid='.$question->id);
213
214 $table->sortable(true);
e99b3759 215 $table->initialbars($usercount>20); // will show initialbars if there are more than 20 users
fbe4d5ce 216 $table->pageable(true);
804d664a 217 $table->collapsible(true);
fbe4d5ce 218
219 $table->column_suppress('fullname');
220 $table->column_suppress('picture');
804d664a 221 $table->column_suppress('grade');
fbe4d5ce 222
223 $table->column_class('picture', 'picture');
224
225 // attributes in the table tag
226 $table->set_attribute('cellspacing', '0');
804d664a 227 $table->set_attribute('id', 'attempts');
fbe4d5ce 228 $table->set_attribute('class', 'generaltable generalbox');
229 $table->set_attribute('align', 'center');
804d664a 230 //$table->set_attribute('width', '50%');
fbe4d5ce 231
232 // get it ready!
233 $table->setup();
234
235 // this sql is a join of the attempts table and the user table. I do this so I can sort by user name and attempt number (not id)
27176468 236 $select = 'SELECT '.sql_concat('u.id', '\'#\'', $db->IfNull('qa.attempt', '0')).' AS userattemptid, qa.id AS attemptid, qa.uniqueid, qa.attempt, qa.timefinish, u.id AS userid, u.firstname, u.lastname, u.picture ';
fbe4d5ce 237 $from = 'FROM '.$CFG->prefix.'user u LEFT JOIN '.$CFG->prefix.'quiz_attempts qa ON (u.id = qa.userid AND qa.quiz = '.$quiz->id.') ';
e99b3759 238 $where = 'WHERE u.id IN ('.$userids.') ';
fbe4d5ce 239 $where .= 'AND '.$db->IfNull('qa.attempt', '0').' != 0 ';
240 $where .= 'AND '.$db->IfNull('qa.timefinish', '0').' != 0 ';
241 $where .= 'AND preview = 0 '; // ignore previews
9d4cbe56 242
fbe4d5ce 243 if($table->get_sql_where()) { // forgot what this does
244 $where .= 'AND '.$table->get_sql_where();
245 }
246
247 // sorting of the table
248 if($sort = $table->get_sql_sort()) {
249 $sort = 'ORDER BY '.$sort; // seems like I would need to have u. or qa. infront of the ORDER BY attribues... but seems to work..
250 } else {
251 // my default sort rule
bc67d9b3 252 $sort = 'ORDER BY u.firstname, u.lastname, qa.timefinish ASC';
fbe4d5ce 253 }
254
255 // set up the pagesize
27176468 256 $total = count_records_sql('SELECT COUNT(DISTINCT('.sql_concat('u.id', '\'#\'', $db->IfNull('qa.attempt', '0')).')) '.$from.$where);
f33c438e 257 $table->pagesize(QUIZ_REPORT_DEFAULT_PAGE_SIZE, $total);
fbe4d5ce 258
fbe4d5ce 259 // get the attempts and process them
1ad5c638 260 if ($attempts = get_records_sql($select.$from.$where.$sort,$table->get_page_start(), $table->get_page_size())) {
fbe4d5ce 261 foreach($attempts as $attempt) {
262
263 $picture = print_user_picture($attempt->userid, $quiz->course, $attempt->picture, false, true);
264
804d664a 265 // link to student profile
266 $userlink = "<a href=\"$CFG->wwwroot/user/view.php?id=$attempt->userid&amp;course=$quiz->course\">".
fbe4d5ce 267 fullname($attempt, true).'</a>';
268
e99b3759 269 if (!$this->is_graded($question, $attempt)) {
fbe4d5ce 270 $style = 'class="manual-ungraded"';
271 } else {
272 $style = 'class="manual-graded"';
273 }
274
275 // link for the attempt
e99b3759 276 $attemptlink = "<a $style href=\"report.php?mode=grading&amp;action=grade&amp;q=$quiz->id&amp;questionid=$question->id&amp;attemptid=$attempt->attemptid\">".
804d664a 277 userdate($attempt->timefinish, get_string('strftimedatetime')).'</a>';
bc67d9b3 278
804d664a 279 // grade all attempts for this user
280 $gradelink = "<a href=\"report.php?mode=grading&amp;action=grade&amp;q=$quiz->id&amp;questionid=$question->id&amp;userid=$attempt->userid\">".
281 get_string('grade').'</a>';
fbe4d5ce 282
804d664a 283 $table->add_data( array($picture, $userlink, $attemptlink, $gradelink) );
fbe4d5ce 284 }
285 }
286
287 // grade all and "back" links
09275894 288 $links = "<div class=\"boxaligncenter\"><a href=\"report.php?mode=grading&amp;action=grade&amp;q=$quiz->id&amp;questionid=$question->id&amp;gradeall=1\">".get_string('gradeall', 'quiz').'</a> | '.
289 "<a href=\"report.php?mode=grading&amp;q=$quiz->id&amp;action=viewquestions\">".get_string('backtoquestionlist', 'quiz').'</a></div>'.
fbe4d5ce 290
291 // print everything here
292 print_heading($question->name);
293 echo $links;
294 echo '<div id="tablecontainer">';
295 $table->print_html();
296 echo '</div>';
297 echo $links;
298 }
9d4cbe56 299
fbe4d5ce 300 /**
e99b3759 301 * Checks to see if a question in a particular attempt is graded
fbe4d5ce 302 *
e99b3759 303 * @return boolean
304 * @todo Finnish documenting this function
fbe4d5ce 305 **/
e99b3759 306 function is_graded($question, $attempt) {
fbe4d5ce 307 global $CFG;
9d4cbe56 308
309 if (!$state = get_record_sql("SELECT state.id, state.event FROM
310 {$CFG->prefix}question_states state, {$CFG->prefix}question_sessions sess
311 WHERE sess.newest = state.id AND
fbe4d5ce 312 sess.attemptid = $attempt->uniqueid AND
313 sess.questionid = $question->id")) {
5a2a5331 314 print_error('Could not find question state');
fbe4d5ce 315 }
9d4cbe56 316
e99b3759 317 return question_state_is_graded($state);
318 }
9d4cbe56 319
e99b3759 320 /**
321 * Prints questions with comment and grade form underneath each question
322 *
323 * @return void
324 * @todo Finish documenting this function
325 **/
326 function print_questions_and_form($quiz, $question) {
327 global $CFG, $db;
9d4cbe56 328
e99b3759 329 // grade question specific parameters
330 $gradeall = optional_param('gradeall', 0, PARAM_INT);
331 $userid = optional_param('userid', 0, PARAM_INT);
332 $attemptid = optional_param('attemptid', 0, PARAM_INT);
9d4cbe56 333
cd06115f 334 // TODO get the context, and put in proper roles an permissions checks.
335 $context = NULL;
336
e99b3759 337 $questions[$question->id] = &$question;
338 $usehtmleditor = can_use_richtext_editor();
339 $users = get_course_students($quiz->course);
340 $userids = implode(',', array_keys($users));
9d4cbe56 341
e99b3759 342 // this sql joins the attempts table and the user table
27176468 343 $select = 'SELECT '.sql_concat('u.id', '\'#\'', $db->IfNull('qa.attempt', '0')).' AS userattemptid,
e99b3759 344 qa.id AS attemptid, qa.uniqueid, qa.attempt, qa.timefinish, qa.preview,
345 u.id AS userid, u.firstname, u.lastname, u.picture ';
346 $from = 'FROM '.$CFG->prefix.'user u LEFT JOIN '.$CFG->prefix.'quiz_attempts qa ON (u.id = qa.userid AND qa.quiz = '.$quiz->id.') ';
347
348 if ($gradeall) { // get all user attempts
349 $where = 'WHERE u.id IN ('.$userids.') ';
350 } else if ($userid) { // get all the attempts for a specific user
351 $where = 'WHERE u.id='.$userid.' ';
352 } else { // get a specific attempt
353 $where = 'WHERE qa.id='.$attemptid.' ';
354 }
355
356 // ignore previews
357 $where .= ' AND preview = 0 ';
358
359 $where .= 'AND '.$db->IfNull('qa.attempt', '0').' != 0 ';
360 $where .= 'AND '.$db->IfNull('qa.timefinish', '0').' != 0 ';
361 $sort = 'ORDER BY u.firstname, u.lastname, qa.attempt ASC';
362 $attempts = get_records_sql($select.$from.$where.$sort);
363
364 // Display the form with one part for each selected attempt
365
366 echo '<form method="post" action="report.php">'.
60af2703 367 '<input type="hidden" name="mode" value="grading" />'.
6c554fc9 368 '<input type="hidden" name="q" value="'.$quiz->id.'" />'.
60af2703 369 '<input type="hidden" name="sesskey" value="'.sesskey().'" />'.
370 '<input type="hidden" name="action" value="viewquestion" />'.
6c554fc9 371 '<input type="hidden" name="questionid" value="'.$question->id.'" />';
e99b3759 372
373 foreach ($attempts as $attempt) {
374
375 // Load the state for this attempt (The questions array was created earlier)
376 $states = get_question_states($questions, $quiz, $attempt);
377 // The $states array is indexed by question id but because we are dealing
378 // with only one question there is only one entry in this array
379 $state = &$states[$question->id];
380
cd06115f 381 $options = quiz_get_reviewoptions($quiz, $attempt, $context);
e99b3759 382 unset($options->questioncommentlink);
3e3e5a40 383 $copy = $state->manualcomment;
384 $state->manualcomment = '';
e99b3759 385
386 $options->readonly = 1;
387
388 // print the user name, attempt count, the question, and some more hidden fields
09275894 389 echo '<div class="boxaligncenter" width="80%" style="padding:15px;">'.
e99b3759 390 fullname($attempt, true).': '.
391 get_string('attempt', 'quiz').$attempt->attempt;
392
393 print_question($question, $state, '', $quiz, $options);
9d4cbe56 394
caca24d5 395 $prefix = "manualgrades[$attempt->uniqueid]";
396 $grade = round($state->last_graded->grade, 3);
3e3e5a40 397 $state->manualcomment = $copy;
9d4cbe56 398
eeaeff8e 399 include($CFG->dirroot . '/question/comment.html');
9d4cbe56 400
e99b3759 401 echo '</div>';
402 }
09275894 403 echo '<div class="boxaligncenter"><input type="submit" value="'.get_string('savechanges').'" /></div>'.
e99b3759 404 '</form>';
9d4cbe56 405
e99b3759 406 if ($usehtmleditor) {
407 use_html_editor();
408 }
fbe4d5ce 409 }
31e95855 410
411}
412
413?>