diff --git a/lang/en_utf8/quiz.php b/lang/en_utf8/quiz.php
index cb4c3f01d41..84beb2b6500 100644
--- a/lang/en_utf8/quiz.php
+++ b/lang/en_utf8/quiz.php
@@ -90,6 +90,7 @@ $string['cannotloadquestion'] = 'Could not load question options';
$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';
@@ -524,7 +525,6 @@ $string['reviewbefore'] = 'Allow review while quiz is open';
$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';
diff --git a/lib/questionlib.php b/lib/questionlib.php
index 9e7f56ad071..300dc0b261b 100644
--- a/lib/questionlib.php
+++ b/lib/questionlib.php
@@ -37,6 +37,10 @@ define('QUESTION_EVENTMANUALGRADE', '9'); // Grade was entered by teacher
define('QUESTION_EVENTS_GRADED', QUESTION_EVENTGRADE.','.
QUESTION_EVENTCLOSEANDGRADE.','.
QUESTION_EVENTMANUALGRADE);
+global $QUESTION_EVENTS_GRADED;
+$QUESTION_EVENTS_GRADED = array(QUESTION_EVENTGRADE, QUESTION_EVENTCLOSEANDGRADE,
+ QUESTION_EVENTMANUALGRADE);
+
/**#@-*/
/**#@+
@@ -792,6 +796,12 @@ function question_preload_questions($questionids, $extrafields = '', $join = '',
$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;
}
@@ -995,6 +1005,53 @@ function get_question_states(&$questions, $cmoptions, $attempt, $lastattemptid =
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
diff --git a/mod/quiz/attemptlib.php b/mod/quiz/attemptlib.php
index b972db15222..31b03ff3119 100644
--- a/mod/quiz/attemptlib.php
+++ b/mod/quiz/attemptlib.php
@@ -419,6 +419,16 @@ class quiz_attempt extends quiz {
$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() {
@@ -450,6 +460,20 @@ class quiz_attempt extends quiz {
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];
@@ -576,12 +600,9 @@ class quiz_attempt extends quiz {
* @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);
}
@@ -619,6 +640,22 @@ class quiz_attempt extends quiz {
$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[] = '' . $at->attempt . '';
+ } else {
+ $changedurl = preg_replace($search, 'attempt=' . $at->id, $url);
+ $attemptlist[] = '' . $at->attempt . '';
+ }
+ }
+ 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) {
@@ -750,7 +787,7 @@ abstract class quiz_nav_panel_base {
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';
}
diff --git a/mod/quiz/locallib.php b/mod/quiz/locallib.php
index be9209536cd..6b845bebbd2 100644
--- a/mod/quiz/locallib.php
+++ b/mod/quiz/locallib.php
@@ -821,7 +821,9 @@ function quiz_get_reviewoptions($quiz, $attempt, $context=null) {
$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)) {
diff --git a/mod/quiz/report/overview/overview_table.php b/mod/quiz/report/overview/overview_table.php
index fbd3ac6d467..ce2e69ef439 100644
--- a/mod/quiz/report/overview/overview_table.php
+++ b/mod/quiz/report/overview/overview_table.php
@@ -190,6 +190,14 @@ class quiz_report_overview_table extends table_sql {
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)){
@@ -211,8 +219,8 @@ class quiz_report_overview_table extends table_sql {
$grade = ''.$oldgrade.'
'.
$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 {
diff --git a/mod/quiz/review.php b/mod/quiz/review.php
index b5fd3f90718..be4a2ae184a 100644
--- a/mod/quiz/review.php
+++ b/mod/quiz/review.php
@@ -19,16 +19,10 @@
/// 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.
@@ -36,8 +30,8 @@
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) {
@@ -46,10 +40,6 @@
}
}
-/// 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();
@@ -59,8 +49,12 @@
$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());
@@ -78,7 +72,7 @@
/// Print tabs if they should be there.
if ($attemptobj->is_preview_user()) {
- if ($reviewofownattempt) {
+ if ($attemptobj->is_own_attempt()) {
$currenttab = 'preview';
} else {
$currenttab = 'reports';
@@ -88,8 +82,8 @@
}
/// 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);
@@ -139,20 +133,12 @@
$CFG->wwwroot . '/user/view.php?id=' . $student->id . '&course=' . $attemptobj->get_courseid() . '">' .
fullname($student, true) . '';
}
- 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[] = '' . $at->attempt . '';
- } else {
- $attemptlist[] = '' . $at->attempt . '';
- }
+ if ($attemptobj->has_capability('mod/quiz:viewreports')) {
+ $attemptlist = $attemptobj->links_to_other_attempts($attemptobj->review_url(0, $page, $showall));
+ if ($attemptlist) {
+ $rows[] = '