MDL-61033 quiz: Editing a quiz while a preview is open

Editing a quiz while a preview is open in another browser tab can leads to a cryptic error that is hard to recover from
This commit is contained in:
dinhloc 2018-01-09 09:38:54 +07:00 committed by hieuvu
parent a15c745936
commit f4695ec722
13 changed files with 156 additions and 44 deletions

View File

@ -39,8 +39,9 @@ if ($id = optional_param('id', 0, PARAM_INT)) {
// Get submitted parameters.
$attemptid = required_param('attempt', PARAM_INT);
$page = optional_param('page', 0, PARAM_INT);
$cmid = optional_param('cmid', null, PARAM_INT);
$attemptobj = quiz_attempt::create($attemptid);
$attemptobj = quiz_create_attempt_handling_errors($attemptid, $cmid);
$page = $attemptobj->force_page_number_into_range($page);
$PAGE->set_url($attemptobj->attempt_url(null, $page));

View File

@ -340,6 +340,7 @@ class quiz {
if ($page) {
$url .= '&page=' . $page;
}
$url .= '&cmid=' . $this->get_cmid();
return $url;
}
@ -359,7 +360,7 @@ class quiz {
* @return string the URL of the review of that attempt.
*/
public function review_url($attemptid) {
return new moodle_url('/mod/quiz/review.php', array('attempt' => $attemptid));
return new moodle_url('/mod/quiz/review.php', array('attempt' => $attemptid, 'cmid' => $this->get_cmid()));
}
/**
@ -367,7 +368,7 @@ class quiz {
* @return string the URL of the review of that attempt.
*/
public function summary_url($attemptid) {
return new moodle_url('/mod/quiz/summary.php', array('attempt' => $attemptid));
return new moodle_url('/mod/quiz/summary.php', array('attempt' => $attemptid, 'cmid' => $this->get_cmid()));
}
// Bits of content =========================================================
@ -1400,7 +1401,7 @@ class quiz_attempt {
* @return string the URL of this quiz's summary page.
*/
public function summary_url() {
return new moodle_url('/mod/quiz/summary.php', array('attempt' => $this->attempt->id));
return new moodle_url('/mod/quiz/summary.php', array('attempt' => $this->attempt->id, 'cmid' => $this->get_cmid()));
}
/**
@ -2104,7 +2105,7 @@ class quiz_attempt {
} else {
$url = new moodle_url('/mod/quiz/' . $script . '.php' . $fragment,
array('attempt' => $this->attempt->id));
array('attempt' => $this->attempt->id, 'cmid' => $this->get_cmid()));
if ($page == 0 && $showall != $defaultshowall) {
$url->param('showall', (int) $showall);
} else if ($page > 0) {

View File

@ -35,9 +35,10 @@ require_sesskey();
// Get submitted parameters.
$attemptid = required_param('attempt', PARAM_INT);
$thispage = optional_param('thispage', 0, PARAM_INT);
$cmid = optional_param('cmid', null, PARAM_INT);
$transaction = $DB->start_delegated_transaction();
$attemptobj = quiz_attempt::create($attemptid);
$attemptobj = quiz_create_attempt_handling_errors($attemptid, $cmid);
// Check login.
require_login($attemptobj->get_course(), false, $attemptobj->get_cm());

View File

@ -28,10 +28,11 @@ require_once('locallib.php');
$attemptid = required_param('attempt', PARAM_INT);
$slot = required_param('slot', PARAM_INT); // The question number in the attempt.
$cmid = optional_param('cmid', null, PARAM_INT);
$PAGE->set_url('/mod/quiz/comment.php', array('attempt' => $attemptid, 'slot' => $slot));
$attemptobj = quiz_attempt::create($attemptid);
$attemptobj = quiz_create_attempt_handling_errors($attemptid, $cmid);
$student = $DB->get_record('user', array('id' => $attemptobj->get_userid()));
// Can only grade finished attempts.

View File

@ -90,6 +90,9 @@ $string['attemptclosed'] = 'Attempt has not closed yet';
$string['attemptduration'] = 'Time taken';
$string['attemptedon'] = 'Attempted on';
$string['attempterror'] = 'You are not allowed to attempt this quiz at this time because: {$a}';
$string['attempterrorinvalid'] = 'Invalid quiz attempt ID';
$string['attempterrorcontentchange'] = 'This quiz attempt no longer exists. (When a quiz is edited in-progress previews are automatically deleted.)';
$string['attempterrorcontentchangeforuser'] = 'This quiz attempt no longer exists.';
$string['attemptfirst'] = 'First attempt';
$string['attemptincomplete'] = 'That attempt (by {$a}) is not yet completed.';
$string['attemptlast'] = 'Last attempt';

View File

@ -2516,4 +2516,36 @@ function quiz_extract_random_question_tag_ids($tagsjson, $matchbyid = true) {
// Only work with tags that exist.
return array_filter(array_column($tags, 'id'));
}
}
/**
* Get quiz attempt and handling error.
*
* @param int $attemptid the id of the current attempt.
* @param int|null $cmid the course_module id for this quiz.
* @return quiz_attempt $attemptobj all the data about the quiz attempt.
* @throws moodle_exception
*/
function quiz_create_attempt_handling_errors($attemptid, $cmid = null) {
try {
$attempobj = quiz_attempt::create($attemptid);
} catch (moodle_exception $e) {
if (!empty($cmid)) {
list($course, $cm) = get_course_and_cm_from_cmid($cmid, 'quiz');
$continuelink = new moodle_url('/mod/quiz/view.php', array('id' => $cmid));
$context = context_module::instance($cm->id);
if (has_capability('mod/quiz:preview', $context)) {
throw new moodle_exception('attempterrorcontentchange', 'quiz', $continuelink);
} else {
throw new moodle_exception('attempterrorcontentchangeforuser', 'quiz', $continuelink);
}
} else {
throw new moodle_exception('attempterrorinvalid', 'quiz');
}
}
if (!empty($cmid) && $attempobj->get_cmid() != $cmid) {
throw new moodle_exception('invalidcoursemodule');
} else {
return $attempobj;
}
}

View File

@ -44,8 +44,9 @@ $next = optional_param('next', false, PARAM_BOOL);
$finishattempt = optional_param('finishattempt', false, PARAM_BOOL);
$timeup = optional_param('timeup', 0, PARAM_BOOL); // True if form was submitted by timer.
$scrollpos = optional_param('scrollpos', '', PARAM_RAW);
$cmid = optional_param('cmid', null, PARAM_INT);
$attemptobj = quiz_attempt::create($attemptid);
$attemptobj = quiz_create_attempt_handling_errors($attemptid, $cmid);
// Set $nexturl now.
if ($next) {

View File

@ -479,7 +479,8 @@ class mod_quiz_renderer extends plugin_renderer_base {
// Start the form.
$output .= html_writer::start_tag('form',
array('action' => $attemptobj->processattempt_url(), 'method' => 'post',
array('action' => new moodle_url($attemptobj->processattempt_url(),
array('cmid' => $attemptobj->get_cmid())), 'method' => 'post',
'enctype' => 'multipart/form-data', 'accept-charset' => 'utf-8',
'id' => 'responseform'));
$output .= html_writer::start_tag('div');
@ -731,6 +732,7 @@ class mod_quiz_renderer extends plugin_renderer_base {
'finishattempt' => 1,
'timeup' => 0,
'slots' => '',
'cmid' => $attemptobj->get_cmid(),
'sesskey' => sesskey(),
);

View File

@ -33,6 +33,7 @@ require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php');
$attemptid = required_param('attempt', PARAM_INT);
$page = optional_param('page', 0, PARAM_INT);
$showall = optional_param('showall', null, PARAM_BOOL);
$cmid = optional_param('cmid', null, PARAM_INT);
$url = new moodle_url('/mod/quiz/review.php', array('attempt'=>$attemptid));
if ($page !== 0) {
@ -42,7 +43,7 @@ if ($page !== 0) {
}
$PAGE->set_url($url);
$attemptobj = quiz_attempt::create($attemptid);
$attemptobj = quiz_create_attempt_handling_errors($attemptid, $cmid);
$page = $attemptobj->force_page_number_into_range($page);
// Now we can validate the params better, re-genrate the page URL.

View File

@ -30,6 +30,7 @@ require_once('locallib.php');
$attemptid = required_param('attempt', PARAM_INT);
$slot = required_param('slot', PARAM_INT);
$seq = optional_param('step', null, PARAM_INT);
$cmid = optional_param('cmid', null, PARAM_INT);
$baseurl = new moodle_url('/mod/quiz/reviewquestion.php',
array('attempt' => $attemptid, 'slot' => $slot));
@ -39,7 +40,7 @@ if (!is_null($seq)) {
}
$PAGE->set_url($currenturl);
$attemptobj = quiz_attempt::create($attemptid);
$attemptobj = quiz_create_attempt_handling_errors($attemptid, $cmid);
// Check login.
require_login($attemptobj->get_course(), false, $attemptobj->get_cm());

View File

@ -27,10 +27,11 @@ require_once(__DIR__ . '/../../config.php');
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
$attemptid = required_param('attempt', PARAM_INT); // The attempt to summarise.
$cmid = optional_param('cmid', null, PARAM_INT);
$PAGE->set_url('/mod/quiz/summary.php', array('attempt' => $attemptid));
$attemptobj = quiz_attempt::create($attemptid);
$attemptobj = quiz_create_attempt_handling_errors($attemptid, $cmid);
// Check login.
require_login($attemptobj->get_course(), false, $attemptobj->get_cm());

View File

@ -105,19 +105,19 @@ class mod_quiz_attempt_testcase extends basic_testcase {
// Attempt pages.
$this->assertEquals(new moodle_url(
'/mod/quiz/attempt.php?attempt=123'),
'/mod/quiz/attempt.php?attempt=123&cmid=0'),
$attempt->attempt_url());
$this->assertEquals(new moodle_url(
'/mod/quiz/attempt.php?attempt=123&page=2'),
'/mod/quiz/attempt.php?attempt=123&page=2&cmid=0'),
$attempt->attempt_url(null, 2));
$this->assertEquals(new moodle_url(
'/mod/quiz/attempt.php?attempt=123&page=1#'),
'/mod/quiz/attempt.php?attempt=123&page=1&cmid=0#'),
$attempt->attempt_url(3));
$this->assertEquals(new moodle_url(
'/mod/quiz/attempt.php?attempt=123&page=1#q4'),
'/mod/quiz/attempt.php?attempt=123&page=1&cmid=0#q4'),
$attempt->attempt_url(4));
$this->assertEquals(new moodle_url(
@ -134,52 +134,52 @@ class mod_quiz_attempt_testcase extends basic_testcase {
// Summary page.
$this->assertEquals(new moodle_url(
'/mod/quiz/summary.php?attempt=123'),
'/mod/quiz/summary.php?attempt=123&cmid=0'),
$attempt->summary_url());
// Review page.
$this->assertEquals(new moodle_url(
'/mod/quiz/review.php?attempt=123'),
'/mod/quiz/review.php?attempt=123&cmid=0'),
$attempt->review_url());
$this->assertEquals(new moodle_url(
'/mod/quiz/review.php?attempt=123&page=2'),
'/mod/quiz/review.php?attempt=123&page=2&cmid=0'),
$attempt->review_url(null, 2));
$this->assertEquals(new moodle_url(
'/mod/quiz/review.php?attempt=123&page=1'),
'/mod/quiz/review.php?attempt=123&page=1&cmid=0'),
$attempt->review_url(3, -1, false));
$this->assertEquals(new moodle_url(
'/mod/quiz/review.php?attempt=123&page=1#q4'),
'/mod/quiz/review.php?attempt=123&page=1&cmid=0#q4'),
$attempt->review_url(4, -1, false));
$this->assertEquals(new moodle_url(
'/mod/quiz/review.php?attempt=123'),
'/mod/quiz/review.php?attempt=123&cmid=0'),
$attempt->review_url(null, 2, true));
$this->assertEquals(new moodle_url(
'/mod/quiz/review.php?attempt=123'),
'/mod/quiz/review.php?attempt=123&cmid=0'),
$attempt->review_url(1, -1, true));
$this->assertEquals(new moodle_url(
'/mod/quiz/review.php?attempt=123&page=2'),
'/mod/quiz/review.php?attempt=123&page=2&cmid=0'),
$attempt->review_url(null, 2, false));
$this->assertEquals(new moodle_url(
'/mod/quiz/review.php?attempt=123&showall=0'),
'/mod/quiz/review.php?attempt=123&showall=0&cmid=0'),
$attempt->review_url(null, 0, false));
$this->assertEquals(new moodle_url(
'/mod/quiz/review.php?attempt=123&showall=0'),
'/mod/quiz/review.php?attempt=123&showall=0&cmid=0'),
$attempt->review_url(1, -1, false));
$this->assertEquals(new moodle_url(
'/mod/quiz/review.php?attempt=123&page=1'),
'/mod/quiz/review.php?attempt=123&page=1&cmid=0'),
$attempt->review_url(3, -1, false));
$this->assertEquals(new moodle_url(
'/mod/quiz/review.php?attempt=123&page=2'),
'/mod/quiz/review.php?attempt=123&page=2&cmid=0'),
$attempt->review_url(null, 2));
$this->assertEquals(new moodle_url(
@ -203,7 +203,7 @@ class mod_quiz_attempt_testcase extends basic_testcase {
$attempt->review_url(1, -1, true, 0));
$this->assertEquals(new moodle_url(
'/mod/quiz/review.php?attempt=123&page=2'),
'/mod/quiz/review.php?attempt=123&page=2&cmid=0'),
$attempt->review_url(null, 2, false, 0));
$this->assertEquals(new moodle_url(
@ -215,7 +215,7 @@ class mod_quiz_attempt_testcase extends basic_testcase {
$attempt->review_url(1, -1, false, 0));
$this->assertEquals(new moodle_url(
'/mod/quiz/review.php?attempt=123&page=1#'),
'/mod/quiz/review.php?attempt=123&page=1&cmid=0#'),
$attempt->review_url(3, -1, false, 0));
// Review with more than 50 questions in the quiz.
@ -225,47 +225,47 @@ class mod_quiz_attempt_testcase extends basic_testcase {
'41,42,43,44,45,46,47,48,49,50,0,51,52,53,54,55,56,57,58,59,60,0');
$this->assertEquals(new moodle_url(
'/mod/quiz/review.php?attempt=124'),
'/mod/quiz/review.php?attempt=124&cmid=0'),
$attempt->review_url());
$this->assertEquals(new moodle_url(
'/mod/quiz/review.php?attempt=124&page=2'),
'/mod/quiz/review.php?attempt=124&page=2&cmid=0'),
$attempt->review_url(null, 2));
$this->assertEquals(new moodle_url(
'/mod/quiz/review.php?attempt=124&page=1'),
'/mod/quiz/review.php?attempt=124&page=1&cmid=0'),
$attempt->review_url(11, -1, false));
$this->assertEquals(new moodle_url(
'/mod/quiz/review.php?attempt=124&page=1#q12'),
'/mod/quiz/review.php?attempt=124&page=1&cmid=0#q12'),
$attempt->review_url(12, -1, false));
$this->assertEquals(new moodle_url(
'/mod/quiz/review.php?attempt=124&showall=1'),
'/mod/quiz/review.php?attempt=124&showall=1&cmid=0'),
$attempt->review_url(null, 2, true));
$this->assertEquals(new moodle_url(
'/mod/quiz/review.php?attempt=124&showall=1'),
'/mod/quiz/review.php?attempt=124&showall=1&cmid=0'),
$attempt->review_url(1, -1, true));
$this->assertEquals(new moodle_url(
'/mod/quiz/review.php?attempt=124&page=2'),
'/mod/quiz/review.php?attempt=124&page=2&cmid=0'),
$attempt->review_url(null, 2, false));
$this->assertEquals(new moodle_url(
'/mod/quiz/review.php?attempt=124'),
'/mod/quiz/review.php?attempt=124&cmid=0'),
$attempt->review_url(null, 0, false));
$this->assertEquals(new moodle_url(
'/mod/quiz/review.php?attempt=124&page=1'),
'/mod/quiz/review.php?attempt=124&page=1&cmid=0'),
$attempt->review_url(11, -1, false));
$this->assertEquals(new moodle_url(
'/mod/quiz/review.php?attempt=124&page=1#q12'),
'/mod/quiz/review.php?attempt=124&page=1&cmid=0#q12'),
$attempt->review_url(12, -1, false));
$this->assertEquals(new moodle_url(
'/mod/quiz/review.php?attempt=124&page=2'),
'/mod/quiz/review.php?attempt=124&page=2&cmid=0'),
$attempt->review_url(null, 2));
$this->assertEquals(new moodle_url(
@ -289,7 +289,7 @@ class mod_quiz_attempt_testcase extends basic_testcase {
$attempt->review_url(1, -1, true, 0));
$this->assertEquals(new moodle_url(
'/mod/quiz/review.php?attempt=124&page=2'),
'/mod/quiz/review.php?attempt=124&page=2&cmid=0'),
$attempt->review_url(null, 2, false, 0));
$this->assertEquals(new moodle_url(
@ -301,7 +301,7 @@ class mod_quiz_attempt_testcase extends basic_testcase {
$attempt->review_url(1, -1, false, 0));
$this->assertEquals(new moodle_url(
'/mod/quiz/review.php?attempt=124&page=1#'),
'/mod/quiz/review.php?attempt=124&page=1&cmid=0#'),
$attempt->review_url(11, -1, false, 0));
}
}

View File

@ -387,4 +387,71 @@ class mod_quiz_attempt_overdue_testcase extends advanced_testcase {
groups_delete_group_members($course->id);
$this->assertEquals(1200, $DB->get_field('quiz_attempts', 'timecheckstate', array('id'=>$attemptid)));
}
/**
* Test the functions quiz_create_attempt_handling_errors
*/
public function test_quiz_create_attempt_handling_errors() {
$this->resetAfterTest(true);
$this->setAdminUser();
// Make a quiz.
$course = $this->getDataGenerator()->create_course();
$user1 = $this->getDataGenerator()->create_user();
$student = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user($student->id, $course->id, 'student');
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$quiz = $quizgenerator->create_instance(array('course' => $course->id, 'questionsperpage' => 0, 'grade' => 100.0,
'sumgrades' => 2));
// Create questions.
$cat = $questiongenerator->create_question_category();
$saq = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id));
$numq = $questiongenerator->create_question('numerical', null, array('category' => $cat->id));
// Add them to the quiz.
quiz_add_quiz_question($saq->id, $quiz);
quiz_add_quiz_question($numq->id, $quiz);
$quizobj = quiz::create($quiz->id, $user1->id);
$quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
$quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
$timenow = time();
// Create an attempt.
$attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $user1->id);
quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
quiz_attempt_save_started($quizobj, $quba, $attempt);
$result = quiz_create_attempt_handling_errors($attempt->id, $quiz->cmid);
$this->assertEquals($result->get_attemptid(), $attempt->id);
try {
$result = quiz_create_attempt_handling_errors($attempt->id, 9999);
$this->fail('Exception expected due to invalid course module id.');
} catch (moodle_exception $e) {
$this->assertEquals('invalidcoursemodule', $e->errorcode);
}
try {
quiz_create_attempt_handling_errors(9999, $result->get_cmid());
$this->fail('Exception expected due to quiz content change.');
} catch (moodle_exception $e) {
$this->assertEquals('attempterrorcontentchange', $e->errorcode);
}
try {
quiz_create_attempt_handling_errors(9999);
$this->fail('Exception expected due to invalid quiz attempt id.');
} catch (moodle_exception $e) {
$this->assertEquals('attempterrorinvalid', $e->errorcode);
}
// Set up as normal user without permission to view preview.
$this->setUser($student->id);
try {
quiz_create_attempt_handling_errors(9999, $result->get_cmid());
$this->fail('Exception expected due to quiz content change for user without permission.');
} catch (moodle_exception $e) {
$this->assertEquals('attempterrorcontentchangeforuser', $e->errorcode);
}
try {
quiz_create_attempt_handling_errors($attempt->id, 9999);
$this->fail('Exception expected due to invalid course module id for user without permission.');
} catch (moodle_exception $e) {
$this->assertEquals('invalidcoursemodule', $e->errorcode);
}
}
}