MDL-13678 "Change default number of rows per page on quiz reports" Made a new constan...
[moodle.git] / mod / quiz / report / grading / report.php
1 <?php  // $Id$
2 /**
3  * Quiz report to help teachers manually grade quiz questions that need it.
4  *
5  * @package quiz
6  * @subpackage reports
7  */
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
17 require_once($CFG->dirroot . "/mod/quiz/editlib.php");
18 require_once($CFG->libdir . '/tablelib.php');
20 /**
21  * Quiz report to help teachers manually grade quiz questions that need it.
22  *
23  * @package quiz
24  * @subpackage reports
25  */
26 class quiz_report extends quiz_default_report {
27     /**
28      * Displays the report.
29      */
30     function display($quiz, $cm, $course) {
32         $action = optional_param('action', 'viewquestions', PARAM_ALPHA);
33         $questionid = optional_param('questionid', 0, PARAM_INT);
35         $this->print_header_and_tabs($cm, $course, $quiz, $reportmode="grading");
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         }
44         if (!empty($questionid)) {
45             if (! $question = get_record('question', 'id', $questionid)) {
46                 print_error("Question with id $questionid not found");
47             }
48             $question->maxgrade = get_field('quiz_question_instances', 'grade', 'quiz', $quiz->id, 'question', $question->id);
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;
56             // We need to add additional questiontype specific information to
57             // the question objects.
58             if (!get_question_options($questions)) {
59                 print_error("Unable to load questiontype specific question information");
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.
63         }
65         add_to_log($course->id, "quiz", "manualgrading", "report.php?mode=grading&amp;q=$quiz->id", "$quiz->id", "$cm->id");
67         echo '<div id="overDiv" style="position:absolute; visibility:hidden; z-index:1000;"></div>'; // for overlib
69         if ($data = data_submitted()) {  // post data submitted, process it
70             confirm_sesskey();
72             // now go through all of the responses and save them.
73             foreach($data->manualgrades as $uniqueid => $response) {
74                 // get our attempt
75                 if (! $attempt = get_record('quiz_attempts', 'uniqueid', $uniqueid)) {
76                     print_error('No such attempt ID exists');
77                 }
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];
85                 // the following will update the state and attempt
86                 question_process_comment($question, $state, $attempt, $response['comment'], $response['grade']);
88                 // If the state has changed save it and update the quiz grade
89                 if ($state->changed) {
90                     save_question_session($question, $state);
91                     quiz_save_best_grade($quiz, $attempt->userid);
92                 }
93             }
94             notify(get_string('changessaved', 'quiz'), 'notifysuccess');
95         }
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
101         // the second view displays the users who have answered the essay question
102         // and all of their attempts at answering the question
104         // the third prints the question with a comment
105         // and grade form underneath it
107         switch($action) {
108             case 'viewquestions':
109                 $this->view_questions($quiz);
110                 break;
111             case 'viewquestion':
112                 $this->view_question($quiz, $question);
113                 break;
114             case 'grade':
115                 $this->print_questions_and_form($quiz, $question);
116                 break;
117         }
118         return true;
119     }
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
127      * @return boolean
128      * @todo Look for the TODO in this code to see what still needs to be done
129      **/
130     function view_questions($quiz) {
131         global $CFG, $QTYPE_MANUAL;
133         $users = get_course_students($quiz->course);
135         if(empty($users)) {
136             print_heading(get_string("noattempts", "quiz"));
137             return true;
138         }
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();
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)".
156                "   AND q.qtype IN ($QTYPE_MANUAL)".
157                "   ORDER BY q.name";
158         if (empty($questionlist) or !$questions = get_records_sql($sql)) {
159             print_heading(get_string('noessayquestionsfound', 'quiz'));
160             return false;
161         }
163         notify(get_string('essayonly', 'quiz_grading'));
165         // get all the finished attempts by the users
166         $userids = implode(', ', array_keys($users));
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) {
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) {
175                     if (!$this->is_graded($question, $attempt)) {
176                       $ungraded++;
177                     }
178                 }
180                 $table->data[] = array($link, $ungraded);
181             }
182             print_table($table);
183         } else {
184             print_heading(get_string('noattempts', 'quiz'));
185         }
187         return true;
188     }
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      **/
197     function view_question($quiz, $question) {
198         global $CFG, $db;
200         $users     = get_course_students($quiz->course);
201         $userids   = implode(',', array_keys($users));
202         $usercount = count($users);
204         // set up table
205         $tablecolumns = array('picture', 'fullname', 'timefinish', 'grade');
206         $tableheaders = array('', get_string('name'), get_string("completedon", "quiz"), '');
208         $table = new flexible_table('mod-quiz-report-grading');
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);
214         $table->sortable(true);
215         $table->initialbars($usercount>20);  // will show initialbars if there are more than 20 users
216         $table->pageable(true);
217         $table->collapsible(true);
219         $table->column_suppress('fullname');
220         $table->column_suppress('picture');
221         $table->column_suppress('grade');
223         $table->column_class('picture', 'picture');
225         // attributes in the table tag
226         $table->set_attribute('cellspacing', '0');
227         $table->set_attribute('id', 'attempts');
228         $table->set_attribute('class', 'generaltable generalbox');
229         $table->set_attribute('align', 'center');
230         //$table->set_attribute('width', '50%');
232         // get it ready!
233         $table->setup();
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)
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 ';
237         $from   = 'FROM '.$CFG->prefix.'user u LEFT JOIN '.$CFG->prefix.'quiz_attempts qa ON (u.id = qa.userid AND qa.quiz = '.$quiz->id.') ';
238         $where  = 'WHERE u.id IN ('.$userids.') ';
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
243         if($table->get_sql_where()) { // forgot what this does
244             $where .= 'AND '.$table->get_sql_where();
245         }
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
252             $sort = 'ORDER BY u.firstname, u.lastname, qa.timefinish ASC';
253         }
255         // set up the pagesize
256         $total  = count_records_sql('SELECT COUNT(DISTINCT('.sql_concat('u.id', '\'#\'', $db->IfNull('qa.attempt', '0')).')) '.$from.$where);
257         $table->pagesize(QUIZ_REPORT_DEFAULT_PAGE_SIZE, $total);
259         // get the attempts and process them
260         if ($attempts = get_records_sql($select.$from.$where.$sort,$table->get_page_start(), $table->get_page_size())) {
261             foreach($attempts as $attempt) {
263                 $picture = print_user_picture($attempt->userid, $quiz->course, $attempt->picture, false, true);
265                 // link to student profile
266                 $userlink = "<a href=\"$CFG->wwwroot/user/view.php?id=$attempt->userid&amp;course=$quiz->course\">".
267                             fullname($attempt, true).'</a>';
269                 if (!$this->is_graded($question, $attempt)) {
270                     $style = 'class="manual-ungraded"';
271                 } else {
272                     $style = 'class="manual-graded"';
273                 }
275                 // link for the attempt
276                 $attemptlink = "<a $style href=\"report.php?mode=grading&amp;action=grade&amp;q=$quiz->id&amp;questionid=$question->id&amp;attemptid=$attempt->attemptid\">".
277                         userdate($attempt->timefinish, get_string('strftimedatetime')).'</a>';
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>';
283                 $table->add_data( array($picture, $userlink, $attemptlink, $gradelink) );
284             }
285         }
287         // grade all and "back" links
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>'.
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     }
300     /**
301      * Checks to see if a question in a particular attempt is graded
302      *
303      * @return boolean
304      * @todo Finnish documenting this function
305      **/
306     function is_graded($question, $attempt) {
307         global $CFG;
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
312                                         sess.attemptid = $attempt->uniqueid AND
313                                         sess.questionid = $question->id")) {
314             print_error('Could not find question state');
315         }
317         return question_state_is_graded($state);
318     }
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;
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);
334         // TODO get the context, and put in proper roles an permissions checks.
335         $context = NULL;
337         $questions[$question->id] = &$question;
338         $usehtmleditor = can_use_richtext_editor();
339         $users     = get_course_students($quiz->course);
340         $userids   = implode(',', array_keys($users));
342         // this sql joins the attempts table and the user table
343         $select = 'SELECT '.sql_concat('u.id', '\'#\'', $db->IfNull('qa.attempt', '0')).' AS userattemptid,
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.') ';
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         }
356         // ignore previews
357         $where .= ' AND preview = 0 ';
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);
364         // Display the form with one part for each selected attempt
366         echo '<form method="post" action="report.php">'.
367             '<input type="hidden" name="mode" value="grading" />'.
368             '<input type="hidden" name="q" value="'.$quiz->id.'" />'.
369             '<input type="hidden" name="sesskey" value="'.sesskey().'" />'.
370             '<input type="hidden" name="action" value="viewquestion" />'.
371             '<input type="hidden" name="questionid" value="'.$question->id.'" />';
373         foreach ($attempts as $attempt) {
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];
381             $options = quiz_get_reviewoptions($quiz, $attempt, $context);
382             unset($options->questioncommentlink);
383             $copy = $state->manualcomment;
384             $state->manualcomment = '';
386             $options->readonly = 1;
388             // print the user name, attempt count, the question, and some more hidden fields
389             echo '<div class="boxaligncenter" width="80%" style="padding:15px;">'.
390                 fullname($attempt, true).': '.
391                 get_string('attempt', 'quiz').$attempt->attempt;
393             print_question($question, $state, '', $quiz, $options);
395             $prefix         = "manualgrades[$attempt->uniqueid]";
396             $grade          = round($state->last_graded->grade, 3);
397             $state->manualcomment = $copy;
399             include($CFG->dirroot . '/question/comment.html');
401             echo '</div>';
402         }
403         echo '<div class="boxaligncenter"><input type="submit" value="'.get_string('savechanges').'" /></div>'.
404             '</form>';
406         if ($usehtmleditor) {
407             use_html_editor();
408         }
409     }
413 ?>