--- /dev/null
+<?php
+/**
+ * This script lists student attempts and responses
+ *
+ * @version $Id$
+
+ * @author Jean-Michel V�drine, Gustav Delius and others.
+ * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
+ * @package quiz
+ *//** */
+
+require_once($CFG->libdir.'/tablelib.php');
+require_once($CFG->dirroot.'/mod/quiz/report/responses/responsessettings_form.php');
+require_once($CFG->dirroot.'/mod/quiz/report/responses/responses_table.php');
+
+class quiz_responses_report extends quiz_default_report {
+
+ /**
+ * Display the report.
+ */
+ function display($quiz, $cm, $course) {
+ global $CFG, $COURSE, $DB;
+
+ $context = get_context_instance(CONTEXT_MODULE, $cm->id);
+
+ // Work out some display options - whether there is feedback, and whether scores should be shown.
+ $hasfeedback = quiz_has_feedback($quiz->id) && $quiz->grade > 1.e-7 && $quiz->sumgrades > 1.e-7;
+ $fakeattempt = new stdClass();
+ $fakeattempt->preview = false;
+ $fakeattempt->timefinish = $quiz->timeopen;
+ $reviewoptions = quiz_get_reviewoptions($quiz, $fakeattempt, $context);
+ $showgrades = $quiz->grade && $quiz->sumgrades && $reviewoptions->scores;
+
+ $download = optional_param('download', '', PARAM_ALPHA);
+
+ if($attemptids = optional_param('attemptid', array(), PARAM_INT)) {
+ //attempts need to be deleted
+ require_capability('mod/quiz:deleteattempts', $context);
+ $attemptids = optional_param('attemptid', array(), PARAM_INT);
+ foreach($attemptids as $attemptid) {
+ add_to_log($course->id, 'quiz', 'delete attempt', 'report.php?id=' . $cm->id,
+ $attemptid, $cm->id);
+ quiz_delete_attempt($attemptid, $quiz);
+ }
+ //No need for a redirect, any attemptids that do not exist are ignored.
+ //So no problem if the user refreshes and tries to delete the same attempts
+ //twice.
+ }
+
+
+ $pageoptions = array();
+ $pageoptions['id'] = $cm->id;
+ $pageoptions['q'] = $quiz->id;
+ $pageoptions['mode'] = 'responses';
+
+ $reporturl = new moodle_url($CFG->wwwroot.'/mod/quiz/report.php', $pageoptions);
+ $qmsubselect = quiz_report_qm_filter_select($quiz);
+
+
+
+ /// find out current groups mode
+ $currentgroup = groups_get_activity_group($cm, true);
+
+ $mform = new mod_quiz_report_responses_settings($reporturl, array('qmsubselect'=> $qmsubselect, 'quiz'=>$quiz, 'currentgroup'=>$currentgroup));
+ if ($fromform = $mform->get_data()){
+ $attemptsmode = $fromform->attemptsmode;
+ if ($qmsubselect){
+ //control is not on the form if
+ //the grading method is not set
+ //to grade one attempt per user eg. for average attempt grade.
+ $qmfilter = $fromform->qmfilter;
+ } else {
+ $qmfilter = 0;
+ }
+ set_user_preference('quiz_report_pagesize', $fromform->pagesize);
+ $pagesize = $fromform->pagesize;
+ } else {
+ $qmfilter = optional_param('qmfilter', 0, PARAM_INT);
+ $attemptsmode = optional_param('attemptsmode', null, PARAM_INT);
+ if ($attemptsmode === null){
+ //default
+ $attemptsmode = QUIZ_REPORT_ATTEMPTS_ALL;
+ } else if ($currentgroup){
+ //default for when a group is selected
+ if ($attemptsmode === null || $attemptsmode == QUIZ_REPORT_ATTEMPTS_ALL){
+ $attemptsmode = QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH;
+ }
+ } else if (!$currentgroup && $course->id == SITEID) {
+ //force report on front page to show all, unless a group is selected.
+ $attemptsmode = QUIZ_REPORT_ATTEMPTS_ALL;
+ }
+ $pagesize = get_user_preferences('quiz_report_pagesize', 0);
+ }
+ if ($pagesize < 1) {
+ $pagesize = QUIZ_REPORT_DEFAULT_PAGE_SIZE;
+ }
+ // We only want to show the checkbox to delete attempts
+ // if the user has permissions and if the report mode is showing attempts.
+ $candelete = has_capability('mod/quiz:deleteattempts', $context)
+ && ($attemptsmode!= QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH_NO);
+
+
+ $displayoptions = array();
+ $displayoptions['attemptsmode'] = $attemptsmode;
+ $displayoptions['qmfilter'] = $qmfilter;
+
+ //work out the sql for this table.
+ if (!$students = get_users_by_capability($context, 'mod/quiz:attempt','','','','','','',false)){
+ $students = array();
+ } else {
+ $students = array_keys($students);
+ }
+
+ if (empty($currentgroup)) {
+ // all users who can attempt quizzes
+ $allowed = $students;
+ $groupstudents = array();
+ } else {
+ // all users who can attempt quizzes and who are in the currently selected group
+ if (!$groupstudents = get_users_by_capability($context, 'mod/quiz:attempt','','','','',$currentgroup,'',false)){
+ $groupstudents = array();
+ } else {
+ $groupstudents = array_keys($groupstudents);
+ }
+ $allowed = $groupstudents;
+ }
+
+ $questions = quiz_report_load_questions($quiz);
+
+ $table = new quiz_report_responses_table($quiz , $qmsubselect, $groupstudents,
+ $students, $questions, $candelete, $reporturl, $displayoptions);
+ $table->is_downloading($download, get_string('reportresponses','quiz_responses'),
+ "$COURSE->shortname ".format_string($quiz->name,true));
+ if (!$table->is_downloading()) {
+ // Only print headers if not asked to download data
+ $meta = '<link rel="stylesheet" type="text/css" href="'.$CFG->wwwroot.'/mod/quiz/report/responses/styles.css" />'."\n";
+ $this->print_header_and_tabs($cm, $course, $quiz, "responses", $meta);
+ }
+
+ if ($groupmode = groups_get_activity_groupmode($cm)) { // Groups are being used
+ if (!$table->is_downloading()) {
+ groups_print_activity_menu($cm, $reporturl->out(false, $displayoptions));
+ }
+ }
+ // Print information on the number of existing attempts
+ if (!$table->is_downloading()) { //do not print notices when downloading
+ if ($strattemptnum = quiz_num_attempt_summary($quiz, $cm, true, $currentgroup)) {
+ echo '<div class="quizattemptcounts">' . $strattemptnum . '</div>';
+ }
+ }
+ $nostudents = false;
+ if (!$students){
+ notify(get_string('nostudentsyet'));
+ $nostudents = true;
+ }else if ($currentgroup && !$groupstudents){
+ notify(get_string('nostudentsingroup'));
+ $nostudents = true;
+ }
+ if (!$table->is_downloading()) {
+ // Print display options
+ $mform->set_data($displayoptions +compact('pagesize'));
+ $mform->display();
+ }
+
+ if (!$nostudents || ($attemptsmode == QUIZ_REPORT_ATTEMPTS_ALL)){
+ // Print information on the grading method and whether we are displaying
+ //
+ if (!$table->is_downloading()) { //do not print notices when downloading
+ if ($strattempthighlight = quiz_report_highlighting_grading_method($quiz, $qmsubselect, $qmfilter)) {
+ echo '<div class="quizattemptcounts">' . $strattempthighlight . '</div>';
+ }
+ }
+
+
+ $showgrades = $quiz->grade && $quiz->sumgrades && $reviewoptions->scores;
+ $hasfeedback = quiz_has_feedback($quiz->id) && $quiz->grade > 1.e-7 && $quiz->sumgrades > 1.e-7;
+
+
+ // Construct the SQL
+ $fields = $DB->sql_concat('u.id', '\'#\'', 'COALESCE(qa.attempt, \'0\')').' AS uniqueid, '.
+ ($qmsubselect?"($qmsubselect) AS gradedattempt, ":'').
+ 'qa.uniqueid AS attemptuniqueid, qa.id AS attempt, u.id AS userid, u.idnumber, u.firstname,'.
+ ' u.lastname, u.institution, u.department, u.email, u.picture, '.
+ 'qa.sumgrades, qa.timefinish, qa.timestart, qa.timefinish - qa.timestart AS duration ';
+
+ // This part is the same for all cases - join users and quiz_attempts tables
+ $from = '{user} u ';
+ $from .= 'LEFT JOIN {quiz_attempts} qa ON qa.userid = u.id AND qa.quiz = :quizid';
+ $params = array('quizid' => $quiz->id);
+
+ if ($qmsubselect && $qmfilter){
+ $from .= ' AND '.$qmsubselect;
+ }
+ switch ($attemptsmode){
+ case QUIZ_REPORT_ATTEMPTS_ALL:
+ // Show all attempts, including students who are no longer in the course
+ $where = 'qa.id IS NOT NULL AND qa.preview = 0';
+ break;
+ case QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH:
+ // Show only students with attempts
+ list($allowed_usql, $allowed_params) = $DB->get_in_or_equal($allowed, SQL_PARAMS_NAMED, 'u0000');
+ $params += $allowed_params;
+ $where = "u.id $allowed_usql AND qa.preview = 0 AND qa.id IS NOT NULL";
+ break;
+ case QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH_NO:
+ // Show only students without attempts
+ list($allowed_usql, $allowed_params) = $DB->get_in_or_equal($allowed, SQL_PARAMS_NAMED, 'u0000');
+ $params += $allowed_params;
+ $where = "u.id $allowed_usql AND qa.id IS NULL";
+ break;
+ case QUIZ_REPORT_ATTEMPTS_ALL_STUDENTS:
+ // Show all students with or without attempts
+ list($allowed_usql, $allowed_params) = $DB->get_in_or_equal($allowed, SQL_PARAMS_NAMED, 'u0000');
+ $params += $allowed_params;
+ $where = "u.id $allowed_usql AND (qa.preview = 0 OR qa.preview IS NULL)";
+ break;
+ }
+
+ $table->set_count_sql("SELECT COUNT(1) FROM $from WHERE $where", $params);
+
+
+
+ $table->set_sql($fields, $from, $where, $params);
+
+ // Define table columns
+ $columns = array();
+ $headers = array();
+
+
+ if (!$table->is_downloading() && $candelete) {
+ $columns[]= 'checkbox';
+ $headers[]= NULL;
+ }
+
+ if (!$table->is_downloading() && $CFG->grade_report_showuserimage) {
+ $columns[]= 'picture';
+ $headers[]= '';
+ }
+ if (!$table->is_downloading()){
+ $columns[]= 'fullname';
+ $headers[]= get_string('name');
+ } else {
+ $columns[]= 'lastname';
+ $headers[]= get_string('lastname');
+ $columns[]= 'firstname';
+ $headers[]= get_string('firstname');
+ }
+
+ if ($CFG->grade_report_showuseridnumber) {
+ $columns[]= 'idnumber';
+ $headers[]= get_string('idnumber');
+ }
+ if ($table->is_downloading()){
+ $columns[]= 'institution';
+ $headers[]= get_string('institution');
+
+ $columns[]= 'department';
+ $headers[]= get_string('department');
+
+ $columns[]= 'email';
+ $headers[]= get_string('email');
+
+ $columns[]= 'timestart';
+ $headers[]= get_string('startedon', 'quiz');
+
+ $columns[]= 'timefinish';
+ $headers[]= get_string('timecompleted','quiz');
+
+ $columns[]= 'duration';
+ $headers[]= get_string('attemptduration', 'quiz');
+ }
+
+ if ($showgrades) {
+ $columns[] = 'sumgrades';
+ $headers[] = get_string('grade', 'quiz').'/'.$quiz->grade;
+ }
+
+ // we want to display responses for all questions
+ foreach ($questions as $id => $question) {
+ // Ignore questions of zero length
+ $columns[] = 'qsanswer'.$id;
+ $headers[] = '#'.$question->number;
+ }
+
+ if ($hasfeedback) {
+ $columns[] = 'feedbacktext';
+ $headers[] = get_string('feedback', 'quiz');
+ }
+
+ // Load the question type specific information
+ if (!get_question_options($questions)) {
+ print_error('Could not load question options');
+ }
+
+ $table->define_columns($columns);
+ $table->define_headers($headers);
+ $table->sortable(true, 'uniqueid');
+
+ // Set up the table
+ $table->define_baseurl($reporturl->out(false, $displayoptions));
+
+ $table->collapsible(true);
+
+ $table->column_suppress('picture');
+ $table->column_suppress('fullname');
+ $table->column_suppress('idnumber');
+
+ $table->no_sorting('feedbacktext');
+
+ $table->column_class('picture', 'picture');
+ $table->column_class('lastname', 'bold');
+ $table->column_class('firstname', 'bold');
+ $table->column_class('fullname', 'bold');
+ $table->column_class('sumgrades', 'bold');
+
+ $table->set_attribute('id', 'attempts');
+
+ $table->out($pagesize, true);
+ }
+ return true;
+ }
+
+}
+
+
+?>
\ No newline at end of file
--- /dev/null
+<?php // $Id$
+
+class quiz_report_responses_table extends table_sql {
+
+ var $useridfield = 'userid';
+
+ var $reporturl;
+ var $displayoptions;
+
+ function quiz_report_responses_table($quiz , $qmsubselect, $groupstudents,
+ $students, $questions, $candelete, $reporturl, $displayoptions){
+ parent::table_sql('mod-quiz-report-responses-report');
+ $this->quiz = $quiz;
+ $this->qmsubselect = $qmsubselect;
+ $this->groupstudents = $groupstudents;
+ $this->students = $students;
+ $this->questions = $questions;
+ $this->candelete = $candelete;
+ $this->reporturl = $reporturl;
+ $this->displayoptions = $displayoptions;
+ }
+ function build_table(){
+ global $CFG, $DB;
+ if ($this->rawdata) {
+ // Define some things we need later to process raw data from db.
+ $this->strtimeformat = get_string('strftimedatetime');
+ parent::build_table();
+ //end of adding data from attempts data to table / download
+ //now add averages at bottom of table :
+ $params = array($this->quiz->id);
+
+ $this->add_separator();
+ if ($this->is_downloading()){
+ $namekey = 'lastname';
+ } else {
+ $namekey = 'fullname';
+ }
+ }
+ }
+
+ function wrap_html_start(){
+ if (!$this->is_downloading()) {
+ if ($this->candelete) {
+ // Start form
+ $strreallydel = addslashes_js(get_string('deleteattemptcheck','quiz'));
+ echo '<div id="tablecontainer">';
+ echo '<form id="attemptsform" method="post" action="' . $this->reporturl->out(true) .
+ '" onsubmit="confirm(\''.$strreallydel.'\');">';
+ echo '<div style="display: none;">';
+ echo $this->reporturl->hidden_params_out(array(), 0, $this->displayoptions);
+ echo '</div>';
+ echo '<div>';
+ }
+ }
+ }
+ function wrap_html_finish(){
+ if (!$this->is_downloading()) {
+ // Print "Select all" etc.
+ if ($this->candelete) {
+ echo '<table id="commands">';
+ echo '<tr><td>';
+ echo '<a href="javascript:select_all_in(\'DIV\',null,\'tablecontainer\');">'.
+ get_string('selectall', 'quiz').'</a> / ';
+ echo '<a href="javascript:deselect_all_in(\'DIV\',null,\'tablecontainer\');">'.
+ get_string('selectnone', 'quiz').'</a> ';
+ echo ' ';
+ echo '<input type="submit" value="'.get_string('deleteselected', 'quiz_responses').'"/>';
+ echo '</td></tr></table>';
+ // Close form
+ echo '</div>';
+ echo '</form></div>';
+ }
+ }
+ }
+
+
+ function col_checkbox($attempt){
+ if ($attempt->attempt){
+ return '<input type="checkbox" name="attemptid[]" value="'.$attempt->attempt.'" />';
+ } else {
+ return '';
+ }
+ }
+
+ function col_picture($attempt){
+ global $COURSE;
+ return print_user_picture($attempt->userid, $COURSE->id, $attempt->picture, false, true);
+ }
+
+
+ function col_timestart($attempt){
+ if ($attempt->attempt) {
+ $startdate = userdate($attempt->timestart, $this->strtimeformat);
+ if (!$this->is_downloading()) {
+ return '<a href="review.php?q='.$this->quiz->id.'&attempt='.$attempt->attempt.'">'.$startdate.'</a>';
+ } else {
+ return $startdate;
+ }
+ } else {
+ return '-';
+ }
+ }
+ function col_timefinish($attempt){
+ if ($attempt->attempt) {
+ if ($attempt->timefinish) {
+ $timefinish = userdate($attempt->timefinish, $this->strtimeformat);
+ if (!$this->is_downloading()) {
+ return '<a href="review.php?q='.$this->quiz->id.'&attempt='.$attempt->attempt.'">'.$timefinish.'</a>';
+ } else {
+ return $timefinish;
+ }
+ } else {
+ return '-';
+ }
+ } else {
+ return '-';
+ }
+ }
+
+ function col_duration($attempt){
+ if ($attempt->timefinish) {
+ return format_time($attempt->duration);
+ } elseif ($attempt->timestart) {
+ return get_string('unfinished', 'quiz');
+ } else {
+ return '-';
+ }
+ }
+ function col_sumgrades($attempt){
+ if ($attempt->timefinish) {
+ $grade = quiz_rescale_grade($attempt->sumgrades, $this->quiz);
+ if (!$this->is_downloading()) {
+ $gradehtml = '<a href="review.php?q='.$this->quiz->id.'&attempt='.$attempt->attempt.'">'.$grade.'</a>';
+ if ($this->qmsubselect && $attempt->gradedattempt){
+ $gradehtml = '<div class="highlight">'.$gradehtml.'</div>';
+ }
+ return $gradehtml;
+ } else {
+ return $grade;
+ }
+ } else {
+ return '-';
+ }
+ }
+ function other_cols($colname, $attempt){
+ static $gradedstatesbyattempt = null, $states =array();
+ if ($gradedstatesbyattempt === null){
+ //get all the attempt ids we want to display on this page
+ //or to export for download.
+ $attemptids = array();
+ foreach ($this->rawdata as $attempt){
+ if ($attempt->attemptuniqueid > 0){
+ $attemptids[] = $attempt->attemptuniqueid;
+ $states[$attempt->attemptuniqueid] = get_question_states($this->questions, $this->quiz, $attempt);
+ }
+ }
+ $gradedstatesbyattempt = quiz_get_newgraded_states($attemptids, true, 'qs.id, qs.grade, qs.event, qs.question, qs.attempt');
+ }
+ if (preg_match('/^qsanswer([0-9]+)$/', $colname, $matches)){
+ $questionid = $matches[1];
+ $question = $this->questions[$questionid];
+ $stateforqinattempt = $gradedstatesbyattempt[$attempt->attemptuniqueid][$questionid];
+ $responses = get_question_actual_response($question, $states[$attempt->attemptuniqueid][$questionid]);
+ $response = (!empty($responses)? implode(', ',$responses) : '-');
+ $grade = $stateforqinattempt->grade;
+ if (!$this->is_downloading()) {
+ $format_options = new stdClass;
+ $format_options->para = false;
+ $format_options->noclean = true;
+ $format_options->newlines = false;
+ if ($grade<= 0) {
+ $qclass = 'uncorrect';
+ } elseif ($grade == 1) {
+ $qclass = 'correct';
+ } else {
+ $qclass = 'partialcorrect';
+ }
+ return '<span class="'.$qclass.'">'.format_text($response, FORMAT_MOODLE, $format_options).'</span>';
+ } else {
+ return format_text($response, FORMAT_MOODLE, $format_options);
+ }
+ } else {
+ return NULL;
+ }
+ }
+
+ function col_feedbacktext($attempt){
+ if ($attempt->timefinish) {
+ if (!$this->is_downloading()) {
+ return quiz_report_feedback_for_grade(quiz_rescale_grade($attempt->sumgrades, $this->quiz), $this->quiz->id);
+ } else {
+ return strip_tags(quiz_report_feedback_for_grade(quiz_rescale_grade($attempt->sumgrades, $this->quiz), $this->quiz->id));
+ }
+ } else {
+ return '-';
+ }
+
+ }
+
+ function query_db($pagesize, $useinitialsbar=true){
+ // Add table joins so we can sort by question answer
+ // unfortunately can't join all tables necessary to fetch all answers
+ // to get the state for one question per attempt row we must join two tables
+ // and there is a limit to how many joins you can have in one query. In MySQL it
+ // is 61. This means that when having more than 29 questions the query will fail.
+ // So we join just the tables needed to sort the attempts.
+ if($sort = $this->get_sql_sort()) {
+ $this->sql->from .= ' ';
+ $sortparts = explode(',', $sort);
+ $matches = array();
+ foreach($sortparts as $sortpart) {
+ $sortpart = trim($sortpart);
+ if (preg_match('/^qsanswer([0-9]+)/', $sortpart, $matches)){
+ $qid = intval($matches[1]);
+ $this->sql->fields .= ", qs$qid.grade AS qsgrade$qid, qs$qid.answer AS qsanswer$qid, qs$qid.event AS qsevent$qid, qs$qid.id AS qsid$qid";
+ $this->sql->from .= "LEFT JOIN {question_sessions} qns$qid ON qns$qid.attemptid = qa.uniqueid AND qns$qid.questionid = :qid$qid ";
+ $this->sql->from .= "LEFT JOIN {question_states} qs$qid ON qs$qid.id = qns$qid.newgraded ";
+ $this->sql->params['qid'.$qid] = $qid;
+ }
+ }
+ }
+ parent::query_db($pagesize, $useinitialsbar);
+ }
+}
+?>
\ No newline at end of file