MDL-15452 - Put the OU quiz navigation improvements into the Moodle codebase. I was able to factor out some commonalities with review.php along the way.

This commit is contained in:
tjhunt
2008-07-15 15:30:39 +00:00
parent 9ee16191ec
commit b55797b8cc
8 changed files with 222 additions and 157 deletions
+1 -1
View File
@@ -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';
+57
View File
@@ -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
+43 -6
View File
@@ -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[] = '<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) {
@@ -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';
}
+3 -1
View File
@@ -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)) {
+10 -2
View File
@@ -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 = '<del>'.$oldgrade.'</del><br />'.
$newgrade;
}
return link_to_popup_window('/mod/quiz/reviewquestion.php?state='.
$stateforqinattempt->id.'&amp;number='.$question->number,
return link_to_popup_window('/mod/quiz/reviewquestion.php?attempt=' .
$attempt->attempt . '&amp;question=' . $question->id,
'reviewquestion', $grade, 450, 650, get_string('reviewresponse', 'quiz'),
'none', true);
} else {
+15 -29
View File
@@ -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 . '&amp;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.
+89 -116
View File
@@ -1,8 +1,9 @@
<?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
*/
@@ -10,137 +11,109 @@
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
$attemptobj = new quiz_attempt($attemptid);
/// 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();
/// 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();
}
/// Can't review other users' attempts.
if (!$attemptobj->is_own_attempt()) {
notify(get_string('notyourattempt', 'quiz'));
close_window_button();
}
/// Can't review unless Students may review -> Responses option is turned on.
if (!$options->responses) {
notify($accessmanager->cannot_review_message($options));
close_window_button();
}
}
/// Load the questions and states.
$questionids = array($questionid);
$attemptobj->load_questions($questionids);
$attemptobj->load_question_states($questionids);
/// If it was asked for, load another state, instead of the latest.
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->load_specific_question_state($questionid, $stateid);
}
require_login($course->id, false, $cm);
$context = get_context_instance(CONTEXT_MODULE, $cm->id);
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");
}
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");
}
}
if ($attempt->userid != $USER->id) {
print_error('notyourattempt', 'quiz');
}
}
//add_to_log($course->id, 'quiz', 'review', "review.php?id=$cm->id&amp;attempt=$attempt->id", "$quiz->id", "$cm->id");
/// 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
$strquizzes = get_string('modulenameplural', 'quiz');
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 . '&amp;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.'&amp;question='.$question->id.'&amp;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));
}
// Print info box unless it is empty
if ($table->data) {
print_table($table);
}
print_question($question, $state, $number, $quiz, $options);
/// 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>';
}
/// 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 the question in the requested state.
$attemptobj->print_question($questionid);
/// Finish the page
print_footer();
?>
+4 -2
View File
@@ -923,8 +923,10 @@ class default_questiontype {
$link = '<b>'.$st->seq_number.'</b>';
} else {
if(isset($options->questionreviewlink)) {
$link = link_to_popup_window ($options->questionreviewlink.'?state='.$st->id.'&amp;number='.$number,
'reviewquestion', $st->seq_number, 450, 650, $strreviewquestion, 'none', true);
$link = link_to_popup_window($options->questionreviewlink .
'&amp;question=' . $question->id . '&amp;state=' . $st->id,
'reviewquestion', $st->seq_number, 450, 650, $strreviewquestion,
'none', true);
} else {
$link = $st->seq_number;
}