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 | |
17 | require_once($CFG->dirroot . "/mod/quiz/editlib.php"); |
18 | require_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 |
26 | class 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&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&q=$quiz->id&action=viewquestion&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&q='.$quiz->id.'&action=viewquestion&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&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&action=grade&q=$quiz->id&questionid=$question->id&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&action=grade&q=$quiz->id&questionid=$question->id&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&action=grade&q=$quiz->id&questionid=$question->id&gradeall=1\">".get_string('gradeall', 'quiz').'</a> | '. |
289 | "<a href=\"report.php?mode=grading&q=$quiz->id&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 | ?> |