$string['cannotopen'] = 'Cannot open export file ($a)';
$string['cannotread'] = 'Cannot read import file (or file is empty)';
$string['cannotrestore'] = 'Could not restore question sessions';
+$string['cannotreviewopen'] = 'You cannot review this attempt, it is still open.';
$string['cannotsavequestion'] = 'Cannot save question list';
$string['cannotwrite'] = 'Cannot write to export file ($a)';
$string['caseno'] = 'No, case is unimportant';
$string['reviewclosed'] = 'After the quiz is closed';
$string['reviewimmediately'] = 'Immediately after the attempt';
$string['reviewnever'] = 'Never allow review';
-$string['reviewnotallowed'] = 'You are not allowed to review other users\' attempts at this quiz.';
$string['reviewofattempt'] = 'Review of attempt $a';
$string['reviewofpreview'] = 'Review of preview';
$string['reviewopen'] = 'Later, while the quiz is still open';
define('QUESTION_EVENTS_GRADED', QUESTION_EVENTGRADE.','.
QUESTION_EVENTCLOSEANDGRADE.','.
QUESTION_EVENTMANUALGRADE);
+global $QUESTION_EVENTS_GRADED;
+$QUESTION_EVENTS_GRADED = array(QUESTION_EVENTGRADE, QUESTION_EVENTCLOSEANDGRADE,
+ QUESTION_EVENTMANUALGRADE);
+
/**#@-*/
/**#@+
$question->_partiallyloaded = true;
}
+ // Note, a possible optimisation here would be to not load the TEXT fields
+ // (that is, questiontext and generalfeedback) here, and instead load them in
+ // question_load_questions. That would add one DB query, but reduce the amount
+ // of data transferred most of the time. I am not going to do this optimisation
+ // until it is shown to be worthwhile.
+
return $questions;
}
return $states;
}
+/**
+ * Load a particular previous state of a question.
+ *
+ * @param array $question The question to load the state for.
+ * @param object $cmoptions Options from the specifica activity module, e.g. $quiz.
+ * @param object $attempt The attempt for which the question sessions are to be loaded.
+ * @param integer $stateid The id of a specific state of this question.
+ * @return object the requested state. False on error.
+ */
+function question_load_specific_state($question, $cmoptions, $attempt, $stateid) {
+ global $DB, $QUESTION_EVENTS_GRADED;
+ // Load specified states for the question.
+ $sql = 'SELECT st.*, sess.sumpenalty, sess.manualcomment
+ FROM {question_states} st, {question_sessions} sess
+ WHERE st.id = ?
+ AND st.attempt = ?
+ AND sess.attemptid = st.attempt
+ AND st.question = ?
+ AND sess.questionid = st.question';
+ $state = $DB->get_record_sql($sql, array($stateid, $attempt->id, $question->id));
+ if (!$state) {
+ return false;
+ }
+ restore_question_state($question, $state);
+
+ // Load the most recent graded states for the questions before the specified one.
+ list($eventinsql, $params) = $DB->get_in_or_equal($QUESTION_EVENTS_GRADED);
+ $sql = 'SELECT st.*, sess.sumpenalty, sess.manualcomment
+ FROM {question_states} st, {question_sessions} sess
+ WHERE st.seq_number <= ?
+ AND st.attempt = ?
+ AND sess.attemptid = st.attempt
+ AND st.question = ?
+ AND sess.questionid = st.question
+ AND st.event ' . $eventinsql .
+ 'ORDER BY st.seq_number DESC';
+ $gradedstates = $DB->get_records_sql($sql, array_merge(
+ array($state->seq_number, $attempt->id, $question->id), $params), 0, 1);
+ if (empty($gradedstates)) {
+ $state->last_graded = clone($state);
+ } else {
+ $gradedstate = reset($gradedstates);
+ restore_question_state($question, $gradedstate);
+ $state->last_graded = $gradedstate;
+ }
+ return $state;
+}
/**
* Creates the run-time fields for the states
$this->states = $newstates + $this->states;
}
+ public function load_specific_question_state($questionid, $stateid) {
+ global $DB;
+ $state = question_load_specific_state($this->questions[$questionid],
+ $this->quiz, $this->attempt, $stateid);
+ if ($state === false) {
+ throw new moodle_quiz_exception($this, 'invalidstateid');
+ }
+ $this->states[$questionid] = $state;
+ }
+
// Simple getters ======================================================================
/** @return integer the attempt id. */
public function get_attemptid() {
return $this->attempt->preview;
}
+ /**
+ * Is this a student dealing with their own attempt/teacher previewing,
+ * or someone with 'mod/quiz:viewreports' reviewing someone elses attempt.
+ *
+ * @return boolean whether this situation should be treated as someone looking at their own
+ * attempt. The distinction normally only matters when an attempt is being reviewed.
+ */
+ public function is_own_attempt() {
+ global $USER;
+ return $this->attempt->userid == $USER->id &&
+ (!$this->is_preview_user() || $this->attempt->preview);
+
+ }
+
public function get_question_state($questionid) {
$this->ensure_state_loaded($questionid);
return $this->states[$questionid];
* @param $otherattemptid if given, link to another attempt, instead of the one we represent.
* @return string the URL to review this attempt.
*/
- public function review_url($questionid = 0, $page = -1, $showall = false, $otherattemptid = null) {
+ public function review_url($questionid = 0, $page = -1, $showall = false) {
global $CFG;
- if (is_null($otherattemptid)) {
- $otherattemptid = $this->attempt->id;
- }
- return $CFG->wwwroot . '/mod/quiz/review.php?attempt=' . $otherattemptid .
+ return $CFG->wwwroot . '/mod/quiz/review.php?attempt=' . $this->attempt->id .
$this->page_and_question_fragment($questionid, $page, $showall);
}
$panel->display();
}
+ /// List of all this user's attempts for people who can see reports.
+ public function links_to_other_attempts($url) {
+ $search = '/\battempt=' . $this->attempt->id . '\b/';
+ $attempts = quiz_get_user_attempts($this->quiz->id, $this->attempt->userid, 'all');
+ $attemptlist = array();
+ foreach ($attempts as $at) {
+ if ($at->id == $this->attempt->id) {
+ $attemptlist[] = '<strong>' . $at->attempt . '</strong>';
+ } else {
+ $changedurl = preg_replace($search, 'attempt=' . $at->id, $url);
+ $attemptlist[] = '<a href="' . $changedurl . '">' . $at->attempt . '</a>';
+ }
+ }
+ return implode(', ', $attemptlist);
+ }
+
// Private methods =====================================================================
// Check that the state of a particular question is loaded, and if not throw an exception.
private function ensure_state_loaded($id) {
abstract protected function get_end_bits();
protected function get_question_state($question) {
- $state = 'todo'; // TODO
+ $state = 'todo'; // TODO MDL-15653
if ($question->_page == $this->page) {
$state .= ' thispage';
}
$options->readonly = true;
// Provide the links to the question review and comment script
- $options->questionreviewlink = '/mod/quiz/reviewquestion.php';
+ if (!empty($attempt->id)) {
+ $options->questionreviewlink = '/mod/quiz/reviewquestion.php?attempt=' . $attempt->id;
+ }
// Show a link to the comment box only for closed attempts
if ($attempt->timefinish && !is_null($context) && has_capability('mod/quiz:grade', $context)) {
return '-';
}
}
+
+ /**
+ * @param string $colname the name of the column.
+ * @param object $attempt the row of data - see the SQL in display() in
+ * mod/quiz/report/overview/report.php to see what fields are present,
+ * and what they are called.
+ * @return string the contents of the cell.
+ */
function other_cols($colname, $attempt){
if (preg_match('/^qsgrade([0-9]+)$/', $colname, $matches)){
$grade = '<del>'.$oldgrade.'</del><br />'.
$newgrade;
}
- return link_to_popup_window('/mod/quiz/reviewquestion.php?state='.
- $stateforqinattempt->id.'&number='.$question->number,
+ return link_to_popup_window('/mod/quiz/reviewquestion.php?attempt=' .
+ $attempt->attempt . '&question=' . $question->id,
'reviewquestion', $grade, 450, 650, get_string('reviewresponse', 'quiz'),
'none', true);
} else {
/// Check login.
require_login($attemptobj->get_courseid(), false, $attemptobj->get_cm());
-
/// Create an object to manage all the other (non-roles) access rules.
$accessmanager = $attemptobj->get_access_manager(time());
$options = $attemptobj->get_review_options();
-/// Work out if this is a student viewing their own attempt/teacher previewing,
-/// or someone with 'mod/quiz:viewreports' reviewing someone elses attempt.
- $reviewofownattempt = $attemptobj->get_userid() == $USER->id &&
- (!$attemptobj->is_preview_user() || $attemptobj->is_preview());
-
/// Permissions checks for normal users who do not have quiz:viewreports capability.
if (!$attemptobj->has_capability('mod/quiz:viewreports')) {
/// Can't review during the attempt - send them back to the attempt page.
redirect($attemptobj->attempt_url(0, $page));
}
/// Can't review other users' attempts.
- if (!$reviewofownattempt) {
- quiz_error($quiz, 'reviewnotallowed');
+ if (!$attemptobj->is_own_attempt()) {
+ quiz_error($quiz, 'notyourattempt');
}
/// Can't review unless Students may review -> Responses option is turned on.
if (!$options->responses) {
}
}
-/// Log this review.
- add_to_log($attemptobj->get_courseid(), 'quiz', 'review', 'review.php?attempt=' .
- $attemptobj->get_attemptid(), $attemptobj->get_quizid(), $attemptobj->get_cmid());
-
/// load the questions and states needed by this page.
if ($showall) {
$questionids = $attemptobj->get_question_ids();
$attemptobj->load_questions($questionids);
$attemptobj->load_question_states($questionids);
+/// Log this review.
+ add_to_log($attemptobj->get_courseid(), 'quiz', 'review', 'review.php?attempt=' .
+ $attemptobj->get_attemptid(), $attemptobj->get_quizid(), $attemptobj->get_cmid());
+
/// Work out appropriate title.
- if ($attemptobj->is_preview_user() && $reviewofownattempt) {
+ if ($attemptobj->is_preview_user() && $attemptobj->is_own_attempt()) {
$strreviewtitle = get_string('reviewofpreview', 'quiz');
} else {
$strreviewtitle = get_string('reviewofattempt', 'quiz', $attemptobj->get_attempt_number());
/// Print tabs if they should be there.
if ($attemptobj->is_preview_user()) {
- if ($reviewofownattempt) {
+ if ($attemptobj->is_own_attempt()) {
$currenttab = 'preview';
} else {
$currenttab = 'reports';
}
/// Print heading.
- print_heading(format_string($quiz->name));
- if ($attemptobj->is_preview_user() && $reviewofownattempt) {
+ print_heading(format_string($attemptobj->get_quiz_name()));
+ if ($attemptobj->is_preview_user() && $attemptobj->is_own_attempt()) {
$attemptobj->print_restart_preview_button();
}
print_heading($strreviewtitle);
$CFG->wwwroot . '/user/view.php?id=' . $student->id . '&course=' . $attemptobj->get_courseid() . '">' .
fullname($student, true) . '</a></td></tr>';
}
- if (has_capability('mod/quiz:viewreports', $context) &&
- count($attempts = $DB->get_records_select('quiz_attempts', "quiz = ? AND userid = ?", array($quiz->id, $attempt->userid), 'attempt ASC')) > 1) {
- /// List of all this user's attempts for people who can see reports.
- $attemptlist = array();
- foreach ($attempts as $at) {
- if ($at->id == $attempt->id) {
- $attemptlist[] = '<strong>' . $at->attempt . '</strong>';
- } else {
- $attemptlist[] = '<a href="' . $attemptobj->review_url(0, $page, $showall, $at->id) .
- '">' . $at->attempt . '</a>';
- }
+ if ($attemptobj->has_capability('mod/quiz:viewreports')) {
+ $attemptlist = $attemptobj->links_to_other_attempts($attemptobj->review_url(0, $page, $showall));
+ if ($attemptlist) {
+ $rows[] = '<tr><th scope="row" class="cell">' . get_string('attempts', 'quiz') .
+ '</th><td class="cell">' . $attemptlist . '</td></tr>';
}
- $rows[] = '<tr><th scope="row" class="cell">' . get_string('attempts', 'quiz') .
- '</th><td class="cell">' . implode(', ', $attemptlist) . '</td></tr>';
}
/// Timing information.
<?php // $Id$
/**
- * This page prints a review of a particular question attempt
+ * This page prints a review of a particular question attempt.
+ * This page is expected to only be used in a popup window.
*
- * @author Martin Dougiamas and many others.
+ * @author Martin Dougiamas, Tim Hunt and many others.
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
* @package quiz
*/
require_once(dirname(__FILE__) . '/../../config.php');
require_once('locallib.php');
- // Either stateid or (attemptid AND questionid) must be given
+ $attemptid = required_param('attempt', PARAM_INT); // attempt id
+ $questionid = required_param('question', PARAM_INT); // question id
$stateid = optional_param('state', 0, PARAM_INT); // state id
- $attemptid = optional_param('attempt', 0, PARAM_INT); // attempt id
- $questionid = optional_param('question', 0, PARAM_INT); // attempt id
- $number = optional_param('number', 0, PARAM_INT); // question number
- if ($stateid) {
- if (! $state = $DB->get_record('question_states', array('id' => $stateid))) {
- print_error('invalidstateid', 'quiz');
- }
- if (! $attempt = $DB->get_record('quiz_attempts', array('uniqueid' => $state->attempt))) {
- print_error('invalidattemptid', 'quiz');
- }
- } elseif ($attemptid) {
- if (! $attempt = $DB->get_record('quiz_attempts', array('id' => $attemptid))) {
- print_error('invalidattemptid', 'quiz');
- }
- if (! $neweststateid = $DB->get_field('question_sessions', 'newest', array('attemptid' => $attempt->uniqueid, 'questionid' => $questionid))) {
- // newest_state not set, probably because this is an old attempt from the old quiz module code
- if (! $state = $DB->get_record('question_states', array('question' => $questionid, 'attempt' => $attempt->uniqueid))) {
- print_error('invalidquestionid', 'quiz');
- }
- } else {
- if (! $state = $DB->get_record('question_states', array('id' => $neweststateid))) {
- print_error('invalidstateid', 'quiz');
- }
- }
- } else {
- print_error('missingparameter');
- }
- if (! $question = $DB->get_record('question', array('id' => $state->question))) {
- print_error('questionmissing', 'quiz');
- }
- if (! $quiz = $DB->get_record('quiz', array('id' => $attempt->quiz))) {
- print_error('invalidcoursemodule');
- }
- if (! $course = $DB->get_record('course', array('id' => $quiz->course))) {
- print_error('coursemisconf');
- }
- if (! $cm = get_coursemodule_from_instance('quiz', $quiz->id, $course->id)) {
- print_error('invalidcoursemodule');
- }
+ $attemptobj = new quiz_attempt($attemptid);
- require_login($course->id, false, $cm);
- $context = get_context_instance(CONTEXT_MODULE, $cm->id);
+/// Check login.
+ require_login($attemptobj->get_courseid(), false, $attemptobj->get_cm());
- if (!has_capability('mod/quiz:viewreports', $context)) {
- if (!$attempt->timefinish) {
- redirect('attempt.php?attempt='.$attempt->id);
- }
- // If not even responses are to be shown in review then we
- // don't allow any review
- if (!($quiz->review & QUIZ_REVIEW_RESPONSES)) {
- print_error("noreview", "quiz");
+/// Create an object to manage all the other (non-roles) access rules.
+ $accessmanager = $attemptobj->get_access_manager(time());
+ $options = $attemptobj->get_review_options();
+
+/// Permissions checks for normal users who do not have quiz:viewreports capability.
+ if (!$attemptobj->has_capability('mod/quiz:viewreports')) {
+ /// Can't review during the attempt - send them back to the attempt page.
+ if (!$attemptobj->is_finished()) {
+ notify(get_string('cannotreviewopen', 'quiz'));
+ close_window_button();
}
- if ((time() - $attempt->timefinish) > 120) { // always allow review right after attempt
- if ((!$quiz->timeclose or time() < $quiz->timeclose) and !($quiz->review & QUIZ_REVIEW_OPEN)) {
- print_error("noreviewuntil", "quiz", '', userdate($quiz->timeclose));
- }
- if ($quiz->timeclose and time() >= $quiz->timeclose and !($quiz->review & QUIZ_REVIEW_CLOSED)) {
- print_error("noreview", "quiz");
- }
+ /// Can't review other users' attempts.
+ if (!$attemptobj->is_own_attempt()) {
+ notify(get_string('notyourattempt', 'quiz'));
+ close_window_button();
}
- if ($attempt->userid != $USER->id) {
- print_error('notyourattempt', 'quiz');
+ /// Can't review unless Students may review -> Responses option is turned on.
+ if (!$options->responses) {
+ notify($accessmanager->cannot_review_message($options));
+ close_window_button();
}
}
- //add_to_log($course->id, 'quiz', 'review', "review.php?id=$cm->id&attempt=$attempt->id", "$quiz->id", "$cm->id");
+/// Load the questions and states.
+ $questionids = array($questionid);
+ $attemptobj->load_questions($questionids);
+ $attemptobj->load_question_states($questionids);
-/// Print the page header
+/// If it was asked for, load another state, instead of the latest.
+ if ($stateid) {
+ $attemptobj->load_specific_question_state($questionid, $stateid);
+ }
- $strquizzes = get_string('modulenameplural', 'quiz');
+/// Log this review.
+ add_to_log($attemptobj->get_courseid(), 'quiz', 'review', 'reviewquestion.php?attempt=' .
+ $attemptobj->get_attemptid() . '&question=' . $questionid .
+ ($stateid ? '&state=' . $stateid : ''),
+ $attemptobj->get_quizid(), $attemptobj->get_cmid());
+/// Print the page header
print_header();
echo '<div id="overDiv" style="position:absolute; visibility:hidden; z-index:1000;"></div>'; // for overlib
-/// Print heading
- print_heading(format_string($question->name));
-
- $question->maxgrade = $DB->get_field('quiz_question_instances', 'grade', array('quiz' => $quiz->id, 'question' => $question->id));
- // Some of the questions code is optimised to work with several questions
- // at once so it wants the question to be in an array.
- $key = $question->id;
- $questions[$key] = &$question;
- // Add additional questiontype specific information to the question objects.
- if (!get_question_options($questions)) {
- print_error('cannotloadtypeinfo', 'quiz');
- }
-
- $session = $DB->get_record('question_sessions', array('attemptid' => $attempt->uniqueid, 'questionid' => $question->id));
- $state->sumpenalty = $session->sumpenalty;
- $state->manualcomment = $session->manualcomment;
- restore_question_state($question, $state);
- $state->last_graded = $state;
-
- $options = quiz_get_reviewoptions($quiz, $attempt, $context);
-
/// Print infobox
- $table->align = array("right", "left");
- if ($attempt->userid <> $USER->id) {
+ $rows = array();
+
+/// User picture and name.
+ if ($attemptobj->get_userid() <> $USER->id) {
// Print user picture and name
- $student = $DB->get_record('user', array('id' => $attempt->userid));
- $picture = print_user_picture($student, $course->id, $student->picture, false, true);
- $table->data[] = array($picture, fullname($student, true));
+ $student = $DB->get_record('user', array('id' => $attemptobj->get_userid()));
+ $picture = print_user_picture($student, $attemptobj->get_courseid(), $student->picture, false, true);
+ $rows[] = '<tr><th scope="row" class="cell">' . $picture . '</th><td class="cell"><a href="' .
+ $CFG->wwwroot . '/user/view.php?id=' . $student->id . '&course=' . $attemptobj->get_courseid() . '">' .
+ fullname($student, true) . '</a></td></tr>';
}
- // print quiz name
- $table->data[] = array(get_string('modulename', 'quiz').':', format_string($quiz->name));
- if (has_capability('mod/quiz:viewreports', $context) and
- count($attempts = $DB->get_records_select('quiz_attempts', "quiz = ? AND userid =?", array($quiz->id, $attempt->userid), 'attempt ASC')) > 1) {
- // print list of attempts
- $attemptlist = '';
- foreach ($attempts as $at) {
- $attemptlist .= ($at->id == $attempt->id)
- ? '<b>'.$at->attempt.'</b>, '
- : '<a href="reviewquestion.php?attempt='.$at->id.'&question='.$question->id.'&number='.$number.'">'.$at->attempt.'</a>, ';
+
+/// Quiz name.
+ $rows[] = '<tr><th scope="row" class="cell">' . get_string('modulename', 'quiz') .
+ '</th><td class="cell">' . format_string($attemptobj->get_quiz_name()) . '</td></tr>';
+
+/// Question name.
+ $rows[] = '<tr><th scope="row" class="cell">' . get_string('question', 'quiz') .
+ '</th><td class="cell">' . format_string(
+ $attemptobj->get_question($questionid)->name) . '</td></tr>';
+
+/// Other attempts at the quiz.
+ if ($attemptobj->has_capability('mod/quiz:viewreports')) {
+ $attemptlist = $attemptobj->links_to_other_attempts(
+ 'reviewquestion.php?attempt=' . $attemptobj->get_attemptid() .
+ '&question=' . $questionid);
+ if ($attemptlist) {
+ $rows[] = '<tr><th scope="row" class="cell">' . get_string('attempts', 'quiz') .
+ '</th><td class="cell">' . $attemptlist . '</td></tr>';
}
- $table->data[] = array(get_string('attempts', 'quiz').':', trim($attemptlist, ' ,'));
}
- if ($state->timestamp) {
- // print time stamp
- $table->data[] = array(get_string("completedon", "quiz").':', userdate($state->timestamp));
+
+/// Timestamp of this action.
+ $timestamp = $attemptobj->get_question_state($questionid)->timestamp;
+ if ($timestamp) {
+ $rows[] = '<tr><th scope="row" class="cell">' . get_string('completedon', 'quiz') .
+ '</th><td class="cell">' . userdate($timestamp) . '</td></tr>';
}
- // Print info box unless it is empty
- if ($table->data) {
- print_table($table);
+
+/// Now output the summary table, if there are any rows to be shown.
+ if (!empty($rows)) {
+ echo '<table class="generaltable generalbox quizreviewsummary"><tbody>', "\n";
+ echo implode("\n", $rows);
+ echo "\n</tbody></table>\n";
}
- print_question($question, $state, $number, $quiz, $options);
+/// Print the question in the requested state.
+ $attemptobj->print_question($questionid);
+/// Finish the page
print_footer();
-
?>
$link = '<b>'.$st->seq_number.'</b>';
} else {
if(isset($options->questionreviewlink)) {
- $link = link_to_popup_window ($options->questionreviewlink.'?state='.$st->id.'&number='.$number,
- 'reviewquestion', $st->seq_number, 450, 650, $strreviewquestion, 'none', true);
+ $link = link_to_popup_window($options->questionreviewlink .
+ '&question=' . $question->id . '&state=' . $st->id,
+ 'reviewquestion', $st->seq_number, 450, 650, $strreviewquestion,
+ 'none', true);
} else {
$link = $st->seq_number;
}