|
Moodle
2.2.1
http://www.collinsharper.com
|
00001 <?php 00002 // This file is part of Moodle - http://moodle.org/ 00003 // 00004 // Moodle is free software: you can redistribute it and/or modify 00005 // it under the terms of the GNU General Public License as published by 00006 // the Free Software Foundation, either version 3 of the License, or 00007 // (at your option) any later version. 00008 // 00009 // Moodle is distributed in the hope that it will be useful, 00010 // but WITHOUT ANY WARRANTY; without even the implied warranty of 00011 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 00012 // GNU General Public License for more details. 00013 // 00014 // You should have received a copy of the GNU General Public License 00015 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 00016 00029 defined('MOODLE_INTERNAL') || die(); 00030 00031 require_once($CFG->libdir.'/tablelib.php'); 00032 00033 00040 abstract class quiz_attempt_report extends quiz_default_report { 00042 protected $context; 00043 00045 protected $showgrades = null; 00046 00053 protected function should_show_grades($quiz) { 00054 if (!is_null($this->showgrades)) { 00055 return $this->showgrades; 00056 } 00057 00058 if ($quiz->timeclose && time() > $quiz->timeclose) { 00059 $when = mod_quiz_display_options::AFTER_CLOSE; 00060 } else { 00061 $when = mod_quiz_display_options::LATER_WHILE_OPEN; 00062 } 00063 $reviewoptions = mod_quiz_display_options::make_from_quiz($quiz, $when); 00064 00065 $this->showgrades = quiz_has_grades($quiz) && 00066 ($reviewoptions->marks >= question_display_options::MARK_AND_MAX || 00067 has_capability('moodle/grade:viewhidden', $this->context)); 00068 00069 return $this->showgrades; 00070 } 00071 00083 protected function load_relevant_students($cm, $course = null) { 00084 $currentgroup = $this->get_current_group($cm, $course, $this->context); 00085 00086 if ($currentgroup == self::NO_GROUPS_ALLOWED) { 00087 return array($currentgroup, array(), array(), array()); 00088 } 00089 00090 if (!$students = get_users_by_capability($this->context, 00091 array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'), 00092 'u.id, 1', '', '', '', '', '', false)) { 00093 $students = array(); 00094 } else { 00095 $students = array_keys($students); 00096 } 00097 00098 if (empty($currentgroup)) { 00099 return array($currentgroup, $students, array(), $students); 00100 } 00101 00102 // We have a currently selected group. 00103 if (!$groupstudents = get_users_by_capability($this->context, 00104 array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'), 00105 'u.id, 1', '', '', '', $currentgroup, '', false)) { 00106 $groupstudents = array(); 00107 } else { 00108 $groupstudents = array_keys($groupstudents); 00109 } 00110 00111 return array($currentgroup, $students, $groupstudents, $groupstudents); 00112 } 00113 00121 protected function validate_common_options(&$attemptsmode, &$pagesize, $course, $currentgroup) { 00122 if ($currentgroup) { 00123 //default for when a group is selected 00124 if ($attemptsmode === null || $attemptsmode == QUIZ_REPORT_ATTEMPTS_ALL) { 00125 $attemptsmode = QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH; 00126 } 00127 } else if (!$currentgroup && $course->id == SITEID) { 00128 //force report on front page to show all, unless a group is selected. 00129 $attemptsmode = QUIZ_REPORT_ATTEMPTS_ALL; 00130 } else if ($attemptsmode === null) { 00131 //default 00132 $attemptsmode = QUIZ_REPORT_ATTEMPTS_ALL; 00133 } 00134 00135 if ($pagesize < 1) { 00136 $pagesize = QUIZ_REPORT_DEFAULT_PAGE_SIZE; 00137 } 00138 } 00139 00146 protected function add_user_columns($table, &$columns, &$headers) { 00147 global $CFG; 00148 if (!$table->is_downloading() && $CFG->grade_report_showuserimage) { 00149 $columns[] = 'picture'; 00150 $headers[] = ''; 00151 } 00152 if (!$table->is_downloading()) { 00153 $columns[] = 'fullname'; 00154 $headers[] = get_string('name'); 00155 } else { 00156 $columns[] = 'lastname'; 00157 $headers[] = get_string('lastname'); 00158 $columns[] = 'firstname'; 00159 $headers[] = get_string('firstname'); 00160 } 00161 00162 // When downloading, some extra fields are always displayed (because 00163 // there's no space constraint) so do not include in extra-field list 00164 $extrafields = get_extra_user_fields($this->context, 00165 $table->is_downloading() ? array('institution', 'department', 'email') : array()); 00166 foreach ($extrafields as $field) { 00167 $columns[] = $field; 00168 $headers[] = get_user_field_name($field); 00169 } 00170 00171 if ($table->is_downloading()) { 00172 $columns[] = 'institution'; 00173 $headers[] = get_string('institution'); 00174 00175 $columns[] = 'department'; 00176 $headers[] = get_string('department'); 00177 00178 $columns[] = 'email'; 00179 $headers[] = get_string('email'); 00180 } 00181 } 00182 00187 protected function configure_user_columns($table) { 00188 $table->column_suppress('picture'); 00189 $table->column_suppress('fullname'); 00190 $table->column_suppress('idnumber'); 00191 00192 $table->column_class('picture', 'picture'); 00193 $table->column_class('lastname', 'bold'); 00194 $table->column_class('firstname', 'bold'); 00195 $table->column_class('fullname', 'bold'); 00196 } 00197 00203 protected function add_time_columns(&$columns, &$headers) { 00204 $columns[] = 'timestart'; 00205 $headers[] = get_string('startedon', 'quiz'); 00206 00207 $columns[] = 'timefinish'; 00208 $headers[] = get_string('timecompleted', 'quiz'); 00209 00210 $columns[] = 'duration'; 00211 $headers[] = get_string('attemptduration', 'quiz'); 00212 } 00213 00222 protected function add_grade_columns($quiz, &$columns, &$headers, $includefeedback = true) { 00223 if ($this->should_show_grades($quiz)) { 00224 $columns[] = 'sumgrades'; 00225 $headers[] = get_string('grade', 'quiz') . '/' . 00226 quiz_format_grade($quiz, $quiz->grade); 00227 } 00228 00229 if ($includefeedback && quiz_has_feedback($quiz)) { 00230 $columns[] = 'feedbacktext'; 00231 $headers[] = get_string('feedback', 'quiz'); 00232 } 00233 } 00234 00244 protected function set_up_table_columns($table, $columns, $headers, $reporturl, 00245 $displayoptions, $collapsible) { 00246 $table->define_columns($columns); 00247 $table->define_headers($headers); 00248 $table->sortable(true, 'uniqueid'); 00249 00250 $table->define_baseurl($reporturl->out(false, $displayoptions)); 00251 00252 $this->configure_user_columns($table); 00253 00254 $table->no_sorting('feedbacktext'); 00255 $table->column_class('sumgrades', 'bold'); 00256 00257 $table->set_attribute('id', 'attempts'); 00258 00259 $table->collapsible($collapsible); 00260 } 00261 00272 protected function delete_selected_attempts($quiz, $cm, $attemptids, $allowed) { 00273 global $DB; 00274 00275 foreach ($attemptids as $attemptid) { 00276 $attempt = $DB->get_record('quiz_attempts', array('id' => $attemptid)); 00277 if (!$attempt || $attempt->quiz != $quiz->id || $attempt->preview != 0) { 00278 // Ensure the attempt exists, and belongs to this quiz. If not skip. 00279 continue; 00280 } 00281 if ($allowed && !in_array($attempt->userid, $allowed)) { 00282 // Ensure the attempt belongs to a student included in the report. If not skip. 00283 continue; 00284 } 00285 add_to_log($quiz->course, 'quiz', 'delete attempt', 'report.php?id=' . $cm->id, 00286 $attemptid, $cm->id); 00287 quiz_delete_attempt($attempt, $quiz); 00288 } 00289 } 00290 } 00291 00298 abstract class quiz_attempt_report_table extends table_sql { 00299 public $useridfield = 'userid'; 00300 00302 protected $reporturl; 00303 00305 protected $displayoptions; 00306 00311 protected $lateststeps = null; 00312 00313 protected $quiz; 00314 protected $context; 00315 protected $qmsubselect; 00316 protected $qmfilter; 00317 protected $attemptsmode; 00318 protected $groupstudents; 00319 protected $students; 00320 protected $questions; 00321 protected $includecheckboxes; 00322 00323 public function __construct($uniqueid, $quiz, $context, $qmsubselect, $qmfilter, 00324 $attemptsmode, $groupstudents, $students, $questions, $includecheckboxes, 00325 $reporturl, $displayoptions) { 00326 parent::__construct($uniqueid); 00327 $this->quiz = $quiz; 00328 $this->context = $context; 00329 $this->qmsubselect = $qmsubselect; 00330 $this->qmfilter = $qmfilter; 00331 $this->attemptsmode = $attemptsmode; 00332 $this->groupstudents = $groupstudents; 00333 $this->students = $students; 00334 $this->questions = $questions; 00335 $this->includecheckboxes = $includecheckboxes; 00336 $this->reporturl = $reporturl; 00337 $this->displayoptions = $displayoptions; 00338 } 00339 00340 public function col_checkbox($attempt) { 00341 if ($attempt->attempt) { 00342 return '<input type="checkbox" name="attemptid[]" value="'.$attempt->attempt.'" />'; 00343 } else { 00344 return ''; 00345 } 00346 } 00347 00348 public function col_picture($attempt) { 00349 global $COURSE, $OUTPUT; 00350 $user = new stdClass(); 00351 $user->id = $attempt->userid; 00352 $user->lastname = $attempt->lastname; 00353 $user->firstname = $attempt->firstname; 00354 $user->imagealt = $attempt->imagealt; 00355 $user->picture = $attempt->picture; 00356 $user->email = $attempt->email; 00357 return $OUTPUT->user_picture($user); 00358 } 00359 00360 public function col_fullname($attempt) { 00361 $html = parent::col_fullname($attempt); 00362 if ($this->is_downloading()) { 00363 return $html; 00364 } 00365 00366 return $html . html_writer::empty_tag('br') . html_writer::link( 00367 new moodle_url('/mod/quiz/review.php', array('attempt' => $attempt->attempt)), 00368 get_string('reviewattempt', 'quiz'), array('class' => 'reviewlink')); 00369 } 00370 00371 public function col_timestart($attempt) { 00372 if ($attempt->attempt) { 00373 return userdate($attempt->timestart, $this->strtimeformat); 00374 } else { 00375 return '-'; 00376 } 00377 } 00378 00379 public function col_timefinish($attempt) { 00380 if ($attempt->attempt && $attempt->timefinish) { 00381 return userdate($attempt->timefinish, $this->strtimeformat); 00382 } else { 00383 return '-'; 00384 } 00385 } 00386 00387 public function col_duration($attempt) { 00388 if ($attempt->timefinish) { 00389 return format_time($attempt->timefinish - $attempt->timestart); 00390 } else if ($attempt->timestart) { 00391 return get_string('unfinished', 'quiz'); 00392 } else { 00393 return '-'; 00394 } 00395 } 00396 00397 public function col_feedbacktext($attempt) { 00398 if (!$attempt->timefinish) { 00399 return '-'; 00400 } 00401 00402 $feedback = quiz_report_feedback_for_grade( 00403 quiz_rescale_grade($attempt->sumgrades, $this->quiz, false), 00404 $this->quiz->id, $this->context); 00405 00406 if ($this->is_downloading()) { 00407 $feedback = strip_tags($feedback); 00408 } 00409 00410 return $feedback; 00411 } 00412 00413 public function get_row_class($attempt) { 00414 if ($this->qmsubselect && $attempt->gradedattempt) { 00415 return 'gradedattempt'; 00416 } else { 00417 return ''; 00418 } 00419 } 00420 00428 public function make_review_link($data, $attempt, $slot) { 00429 global $OUTPUT; 00430 00431 $stepdata = $this->lateststeps[$attempt->usageid][$slot]; 00432 $state = question_state::get($stepdata->state); 00433 00434 $flag = ''; 00435 if ($stepdata->flagged) { 00436 $flag = ' ' . $OUTPUT->pix_icon('i/flagged', get_string('flagged', 'question'), 00437 'moodle', array('class' => 'questionflag')); 00438 } 00439 00440 $feedbackimg = ''; 00441 if ($state->is_finished() && $state != question_state::$needsgrading) { 00442 $feedbackimg = ' ' . $this->icon_for_fraction($stepdata->fraction); 00443 } 00444 00445 $output = html_writer::tag('span', html_writer::tag('span', 00446 $data . $feedbackimg . $flag, 00447 array('class' => $state->get_state_class(true))), array('class' => 'que')); 00448 00449 $url = new moodle_url('/mod/quiz/reviewquestion.php', 00450 array('attempt' => $attempt->attempt, 'slot' => $slot)); 00451 $output = $OUTPUT->action_link($url, $output, 00452 new popup_action('click', $url, 'reviewquestion', 00453 array('height' => 450, 'width' => 650)), 00454 array('title' => get_string('reviewresponse', 'quiz'))); 00455 00456 return $output; 00457 } 00458 00464 protected function icon_for_fraction($fraction) { 00465 global $OUTPUT; 00466 00467 $state = question_state::graded_state_for_fraction($fraction); 00468 if ($state == question_state::$gradedright) { 00469 $icon = 'i/tick_green_big'; 00470 } else if ($state == question_state::$gradedpartial) { 00471 $icon = 'i/tick_amber_big'; 00472 } else { 00473 $icon = 'i/cross_red_big'; 00474 } 00475 00476 return $OUTPUT->pix_icon($icon, get_string($state->get_feedback_class(), 'question'), 00477 'moodle', array('class' => 'icon')); 00478 } 00479 00490 protected function load_question_latest_steps(qubaid_condition $qubaids) { 00491 $dm = new question_engine_data_mapper(); 00492 $latesstepdata = $dm->load_questions_usages_latest_steps( 00493 $qubaids, array_keys($this->questions)); 00494 00495 $lateststeps = array(); 00496 foreach ($latesstepdata as $step) { 00497 $lateststeps[$step->questionusageid][$step->slot] = $step; 00498 } 00499 00500 return $lateststeps; 00501 } 00502 00506 protected function requires_latest_steps_loaded() { 00507 return false; 00508 } 00509 00516 protected function is_latest_step_column($column) { 00517 return false; 00518 } 00519 00525 protected function get_required_latest_state_fields($slot, $alias) { 00526 return ''; 00527 } 00528 00535 public function base_sql($reportstudents) { 00536 global $DB; 00537 00538 $fields = $DB->sql_concat('u.id', "'#'", 'COALESCE(quiza.attempt, 0)') . ' AS uniqueid,'; 00539 00540 if ($this->qmsubselect) { 00541 $fields .= "\n(CASE WHEN $this->qmsubselect THEN 1 ELSE 0 END) AS gradedattempt,"; 00542 } 00543 00544 $extrafields = get_extra_user_fields_sql($this->context, 'u', '', 00545 array('id', 'idnumber', 'firstname', 'lastname', 'picture', 00546 'imagealt', 'institution', 'department', 'email')); 00547 $fields .= ' 00548 quiza.uniqueid AS usageid, 00549 quiza.id AS attempt, 00550 u.id AS userid, 00551 u.idnumber, 00552 u.firstname, 00553 u.lastname, 00554 u.picture, 00555 u.imagealt, 00556 u.institution, 00557 u.department, 00558 u.email' . $extrafields . ', 00559 quiza.sumgrades, 00560 quiza.timefinish, 00561 quiza.timestart, 00562 CASE WHEN quiza.timefinish = 0 THEN null 00563 WHEN quiza.timefinish > quiza.timestart THEN quiza.timefinish - quiza.timestart 00564 ELSE 0 END AS duration'; 00565 // To explain that last bit, in MySQL, qa.timestart and qa.timefinish 00566 // are unsigned. Since MySQL 5.5.5, when they introduced strict mode, 00567 // subtracting a larger unsigned int from a smaller one gave an error. 00568 // Therefore, we avoid doing that. timefinish can be non-zero and less 00569 // than timestart when you have two load-balanced servers with very 00570 // badly synchronised clocks, and a student does a really quick attempt.'; 00571 00572 // This part is the same for all cases - join users and quiz_attempts tables 00573 $from = "\n{user} u"; 00574 $from .= "\nLEFT JOIN {quiz_attempts} quiza ON 00575 quiza.userid = u.id AND quiza.quiz = :quizid"; 00576 $params = array('quizid' => $this->quiz->id); 00577 00578 if ($this->qmsubselect && $this->qmfilter) { 00579 $from .= " AND $this->qmsubselect"; 00580 } 00581 switch ($this->attemptsmode) { 00582 case QUIZ_REPORT_ATTEMPTS_ALL: 00583 // Show all attempts, including students who are no longer in the course 00584 $where = 'quiza.id IS NOT NULL AND quiza.preview = 0'; 00585 break; 00586 case QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH: 00587 // Show only students with attempts 00588 list($usql, $uparams) = $DB->get_in_or_equal( 00589 $reportstudents, SQL_PARAMS_NAMED, 'u'); 00590 $params += $uparams; 00591 $where = "u.id $usql AND quiza.preview = 0 AND quiza.id IS NOT NULL"; 00592 break; 00593 case QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH_NO: 00594 // Show only students without attempts 00595 list($usql, $uparams) = $DB->get_in_or_equal( 00596 $reportstudents, SQL_PARAMS_NAMED, 'u'); 00597 $params += $uparams; 00598 $where = "u.id $usql AND quiza.id IS NULL"; 00599 break; 00600 case QUIZ_REPORT_ATTEMPTS_ALL_STUDENTS: 00601 // Show all students with or without attempts 00602 list($usql, $uparams) = $DB->get_in_or_equal( 00603 $reportstudents, SQL_PARAMS_NAMED, 'u'); 00604 $params += $uparams; 00605 $where = "u.id $usql AND (quiza.preview = 0 OR quiza.preview IS NULL)"; 00606 break; 00607 } 00608 00609 return array($fields, $from, $where, $params); 00610 } 00611 00622 protected function add_latest_state_join($slot) { 00623 $alias = 'qa' . $slot; 00624 00625 $fields = $this->get_required_latest_state_fields($slot, $alias); 00626 if (!$fields) { 00627 return; 00628 } 00629 00630 // This condition roughly filters the list of attempts to be considered. 00631 // It is only used in a subselect to help crappy databases (see MDL-30122) 00632 // therefore, it is better to use a very simple join, which may include 00633 // too many records, than to do a super-accurate join. 00634 $qubaids = new qubaid_join("{quiz_attempts} {$alias}quiza", "{$alias}quiza.uniqueid", 00635 "{$alias}quiza.quiz = :{$alias}quizid", array("{$alias}quizid" => $this->sql->params['quizid'])); 00636 00637 $dm = new question_engine_data_mapper(); 00638 list($inlineview, $viewparams) = $dm->question_attempt_latest_state_view($alias, $qubaids); 00639 00640 $this->sql->fields .= ",\n$fields"; 00641 $this->sql->from .= "\nLEFT JOIN $inlineview ON " . 00642 "$alias.questionusageid = quiza.uniqueid AND $alias.slot = :{$alias}slot"; 00643 $this->sql->params[$alias . 'slot'] = $slot; 00644 $this->sql->params = array_merge($this->sql->params, $viewparams); 00645 } 00646 00652 protected function get_qubaids_condition() { 00653 if (is_null($this->rawdata)) { 00654 throw new coding_exception( 00655 'Cannot call get_qubaids_condition until the main data has been loaded.'); 00656 } 00657 00658 if ($this->is_downloading()) { 00659 // We want usages for all attempts. 00660 return new qubaid_join($this->sql->from, 'quiza.uniqueid', 00661 $this->sql->where, $this->sql->params); 00662 } 00663 00664 $qubaids = array(); 00665 foreach ($this->rawdata as $attempt) { 00666 if ($attempt->usageid > 0) { 00667 $qubaids[] = $attempt->usageid; 00668 } 00669 } 00670 00671 return new qubaid_list($qubaids); 00672 } 00673 00674 public function query_db($pagesize, $useinitialsbar = true) { 00675 $doneslots = array(); 00676 foreach ($this->get_sort_columns() as $column => $notused) { 00677 $slot = $this->is_latest_step_column($column); 00678 if ($slot && !in_array($slot, $doneslots)) { 00679 $this->add_latest_state_join($slot); 00680 $doneslots[] = $slot; 00681 } 00682 } 00683 00684 parent::query_db($pagesize, $useinitialsbar); 00685 00686 if ($this->requires_latest_steps_loaded()) { 00687 $qubaids = $this->get_qubaids_condition(); 00688 $this->lateststeps = $this->load_question_latest_steps($qubaids); 00689 } 00690 } 00691 00692 public function get_sort_columns() { 00693 // Add attemptid as a final tie-break to the sort. This ensures that 00694 // Attempts by the same student appear in order when just sorting by name. 00695 $sortcolumns = parent::get_sort_columns(); 00696 $sortcolumns['quiza.id'] = SORT_ASC; 00697 return $sortcolumns; 00698 } 00699 00700 public function wrap_html_start() { 00701 if ($this->is_downloading() || !$this->includecheckboxes) { 00702 return; 00703 } 00704 00705 $url = new moodle_url($this->reporturl, $this->displayoptions); 00706 $url->param('sesskey', sesskey()); 00707 00708 echo '<div id="tablecontainer">'; 00709 echo '<form id="attemptsform" method="post" action="' . $url->out_omit_querystring() . '">'; 00710 00711 echo html_writer::input_hidden_params($url); 00712 echo '<div>'; 00713 } 00714 00715 public function wrap_html_finish() { 00716 if ($this->is_downloading() || !$this->includecheckboxes) { 00717 return; 00718 } 00719 00720 echo '<div id="commands">'; 00721 echo '<a href="javascript:select_all_in(\'DIV\', null, \'tablecontainer\');">' . 00722 get_string('selectall', 'quiz') . '</a> / '; 00723 echo '<a href="javascript:deselect_all_in(\'DIV\', null, \'tablecontainer\');">' . 00724 get_string('selectnone', 'quiz') . '</a> '; 00725 echo ' '; 00726 $this->submit_buttons(); 00727 echo '</div>'; 00728 // Close form 00729 echo '</div>'; 00730 echo '</form></div>'; 00731 } 00732 00736 protected function submit_buttons() { 00737 global $PAGE; 00738 if (has_capability('mod/quiz:deleteattempts', $this->context)) { 00739 echo '<input type="submit" id="deleteattemptsbutton" name="delete" value="' . 00740 get_string('deleteselected', 'quiz_overview') . '"/>'; 00741 $PAGE->requires->event_handler('#deleteattemptsbutton', 'click', 'M.util.show_confirm_dialog', 00742 array('message' => get_string('deleteattemptcheck', 'quiz'))); 00743 } 00744 } 00745 }