Moodle  2.2.1
http://www.collinsharper.com
C:/xampp/htdocs/moodle/mod/quiz/report/attemptsreport.php
Go to the documentation of this file.
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 '&nbsp;&nbsp;';
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 }
 All Data Structures Namespaces Files Functions Variables Enumerations