diff --git a/mod/quiz/attemptlib.php b/mod/quiz/attemptlib.php index 2da7835dd8d..354407b39b7 100644 --- a/mod/quiz/attemptlib.php +++ b/mod/quiz/attemptlib.php @@ -1333,12 +1333,12 @@ class quiz_attempt { * @param int $timestamp the timestamp that should be stored as the modifed * time in the database for these actions. If null, will use the current time. */ - public function process_submitted_actions($timestamp, $becomingoverdue = false) { + public function process_submitted_actions($timestamp, $becomingoverdue = false, $postdata = null) { global $DB; $transaction = $DB->start_delegated_transaction(); - $this->quba->process_all_actions($timestamp); + $this->quba->process_all_actions($timestamp, $postdata); question_engine::save_questions_usage_by_activity($this->quba); $this->attempt->timemodified = $timestamp; diff --git a/mod/quiz/locallib.php b/mod/quiz/locallib.php index fd9a9fb50bf..1d5d8816e16 100644 --- a/mod/quiz/locallib.php +++ b/mod/quiz/locallib.php @@ -137,6 +137,145 @@ function quiz_create_attempt(quiz $quizobj, $attemptnumber, $lastattempt, $timen return $attempt; } +/** + * Start a normal, new, quiz attempt. + * + * @param quiz $quizobj the quiz object to start an attempt for. + * @param question_usage_by_activity $quba + * @param object $attempt + * @param integer $attemptnumber starting from 1 + * @param integer $timenow the attempt start time + * @return object modified attempt object + * @throws moodle_exception if a random question exhausts the available questions + */ +function quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $timenow) { + // Fully load all the questions in this quiz. + $quizobj->preload_questions(); + $quizobj->load_questions(); + + // Add them all to the $quba. + $idstoslots = array(); + $questionsinuse = array_keys($quizobj->get_questions()); + foreach ($quizobj->get_questions() as $i => $questiondata) { + if ($questiondata->qtype != 'random') { + if (!$quizobj->get_quiz()->shuffleanswers) { + $questiondata->options->shuffleanswers = false; + } + $question = question_bank::make_question($questiondata); + + } else { + $question = question_bank::get_qtype('random')->choose_other_question( + $questiondata, $questionsinuse, $quizobj->get_quiz()->shuffleanswers); + if (is_null($question)) { + throw new moodle_exception('notenoughrandomquestions', 'quiz', + $quizobj->view_url(), $questiondata); + } + } + + $idstoslots[$i] = $quba->add_question($question, $questiondata->maxmark); + $questionsinuse[] = $question->id; + } + + // Start all the questions. + if ($attempt->preview) { + $variantoffset = rand(1, 100); + } else { + $variantoffset = $attemptnumber; + } + $quba->start_all_questions( + new question_variant_pseudorandom_no_repeats_strategy($variantoffset), $timenow); + + // Update attempt layout. + $newlayout = array(); + foreach (explode(',', $attempt->layout) as $qid) { + if ($qid != 0) { + $newlayout[] = $idstoslots[$qid]; + } else { + $newlayout[] = 0; + } + } + $attempt->layout = implode(',', $newlayout); + return $attempt; +} + +/** + * Start a subsequent new attempt, in each attempt builds on last mode. + * + * @param question_usage_by_activity $quba this question usage + * @param object $attempt this attempt + * @param object $lastattempt last attempt + * @return object modified attempt object + * + */ +function quiz_start_attempt_built_on_last($quba, $attempt, $lastattempt) { + $oldquba = question_engine::load_questions_usage_by_activity($lastattempt->uniqueid); + + $oldnumberstonew = array(); + foreach ($oldquba->get_attempt_iterator() as $oldslot => $oldqa) { + $newslot = $quba->add_question($oldqa->get_question(), $oldqa->get_max_mark()); + + $quba->start_question_based_on($newslot, $oldqa); + + $oldnumberstonew[$oldslot] = $newslot; + } + + // Update attempt layout. + $newlayout = array(); + foreach (explode(',', $lastattempt->layout) as $oldslot) { + if ($oldslot != 0) { + $newlayout[] = $oldnumberstonew[$oldslot]; + } else { + $newlayout[] = 0; + } + } + $attempt->layout = implode(',', $newlayout); + return $attempt; +} + +/** + * The save started question usage and quiz attempt in db and log the started attempt. + * + * @param quiz $quizobj + * @param question_usage_by_activity $quba + * @param object $attempt + * @return object attempt object with uniqueid and id set. + */ +function quiz_attempt_save_started($quizobj, $quba, $attempt) { + global $DB; + // Save the attempt in the database. + question_engine::save_questions_usage_by_activity($quba); + $attempt->uniqueid = $quba->get_id(); + $attempt->id = $DB->insert_record('quiz_attempts', $attempt); + // Log the new attempt. + if ($attempt->preview) { + add_to_log($quizobj->get_courseid(), 'quiz', 'preview', 'view.php?id='.$quizobj->get_cmid(), + $quizobj->get_quizid(), $quizobj->get_cmid()); + } else { + add_to_log($quizobj->get_courseid(), 'quiz', 'attempt', 'review.php?attempt='.$attempt->id, + $quizobj->get_quizid(), $quizobj->get_cmid()); + } + return $attempt; +} + +/** + * Fire an event to tell the rest of Moodle a quiz attempt has started. + * + * @param object $attempt + * @param quiz $quizobj + */ +function quiz_fire_attempt_started_event($attempt, $quizobj) { + // Trigger event. + $eventdata = new stdClass(); + $eventdata->component = 'mod_quiz'; + $eventdata->attemptid = $attempt->id; + $eventdata->timestart = $attempt->timestart; + $eventdata->timestamp = $attempt->timestart; + $eventdata->userid = $attempt->userid; + $eventdata->quizid = $quizobj->get_quizid(); + $eventdata->cmid = $quizobj->get_cmid(); + $eventdata->courseid = $quizobj->get_courseid(); + events_trigger('quiz_attempt_started', $eventdata); +} /** * Returns an unfinished attempt (if there is one) for the given diff --git a/mod/quiz/startattempt.php b/mod/quiz/startattempt.php index 4a47194b19a..02652216084 100644 --- a/mod/quiz/startattempt.php +++ b/mod/quiz/startattempt.php @@ -169,111 +169,18 @@ $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); // Create the new attempt and initialize the question sessions $timenow = time(); // Update time now, in case the server is running really slowly. -$attempt = quiz_create_attempt($quizobj, $attemptnumber, $lastattempt, $timenow, - $quizobj->is_preview_user()); +$attempt = quiz_create_attempt($quizobj, $attemptnumber, $lastattempt, $timenow, $quizobj->is_preview_user()); if (!($quizobj->get_quiz()->attemptonlast && $lastattempt)) { - // Starting a normal, new, quiz attempt. - - // Fully load all the questions in this quiz. - $quizobj->preload_questions(); - $quizobj->load_questions(); - - // Add them all to the $quba. - $idstoslots = array(); - $questionsinuse = array_keys($quizobj->get_questions()); - foreach ($quizobj->get_questions() as $i => $questiondata) { - if ($questiondata->qtype != 'random') { - if (!$quizobj->get_quiz()->shuffleanswers) { - $questiondata->options->shuffleanswers = false; - } - $question = question_bank::make_question($questiondata); - - } else { - $question = question_bank::get_qtype('random')->choose_other_question( - $questiondata, $questionsinuse, $quizobj->get_quiz()->shuffleanswers); - if (is_null($question)) { - throw new moodle_exception('notenoughrandomquestions', 'quiz', - $quizobj->view_url(), $questiondata); - } - } - - $idstoslots[$i] = $quba->add_question($question, $questiondata->maxmark); - $questionsinuse[] = $question->id; - } - - // Start all the questions. - if ($attempt->preview) { - $variantoffset = rand(1, 100); - } else { - $variantoffset = $attemptnumber; - } - $quba->start_all_questions( - new question_variant_pseudorandom_no_repeats_strategy($variantoffset), $timenow); - - // Update attempt layout. - $newlayout = array(); - foreach (explode(',', $attempt->layout) as $qid) { - if ($qid != 0) { - $newlayout[] = $idstoslots[$qid]; - } else { - $newlayout[] = 0; - } - } - $attempt->layout = implode(',', $newlayout); - + $attempt = quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $timenow); } else { - // Starting a subsequent attempt in each attempt builds on last mode. - - $oldquba = question_engine::load_questions_usage_by_activity($lastattempt->uniqueid); - - $oldnumberstonew = array(); - foreach ($oldquba->get_attempt_iterator() as $oldslot => $oldqa) { - $newslot = $quba->add_question($oldqa->get_question(), $oldqa->get_max_mark()); - - $quba->start_question_based_on($newslot, $oldqa); - - $oldnumberstonew[$oldslot] = $newslot; - } - - // Update attempt layout. - $newlayout = array(); - foreach (explode(',', $lastattempt->layout) as $oldslot) { - if ($oldslot != 0) { - $newlayout[] = $oldnumberstonew[$oldslot]; - } else { - $newlayout[] = 0; - } - } - $attempt->layout = implode(',', $newlayout); + $attempt = quiz_start_attempt_built_on_last($quba, $attempt, $lastattempt); } -// Save the attempt in the database. $transaction = $DB->start_delegated_transaction(); -question_engine::save_questions_usage_by_activity($quba); -$attempt->uniqueid = $quba->get_id(); -$attempt->id = $DB->insert_record('quiz_attempts', $attempt); -// Log the new attempt. -if ($attempt->preview) { - add_to_log($course->id, 'quiz', 'preview', 'view.php?id=' . $quizobj->get_cmid(), - $quizobj->get_quizid(), $quizobj->get_cmid()); -} else { - add_to_log($course->id, 'quiz', 'attempt', 'review.php?attempt=' . $attempt->id, - $quizobj->get_quizid(), $quizobj->get_cmid()); -} - -// Trigger event. -$eventdata = new stdClass(); -$eventdata->component = 'mod_quiz'; -$eventdata->attemptid = $attempt->id; -$eventdata->timestart = $attempt->timestart; -$eventdata->timestamp = $attempt->timestart; -$eventdata->userid = $attempt->userid; -$eventdata->quizid = $quizobj->get_quizid(); -$eventdata->cmid = $quizobj->get_cmid(); -$eventdata->courseid = $quizobj->get_courseid(); -events_trigger('quiz_attempt_started', $eventdata); +$attempt = quiz_attempt_save_started($quizobj, $quba, $attempt); +quiz_fire_attempt_started_event($attempt, $quizobj); $transaction->allow_commit(); diff --git a/mod/quiz/tests/attempt_walkthrough_test.php b/mod/quiz/tests/attempt_walkthrough_test.php new file mode 100644 index 00000000000..9062af1b82b --- /dev/null +++ b/mod/quiz/tests/attempt_walkthrough_test.php @@ -0,0 +1,126 @@ +. + +/** + * Quiz attempt walk through tests. + * + * @package mod_quiz + * @category phpunit + * @copyright 2013 The Open University + * @author Jamie Pratt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/mod/quiz/editlib.php'); +require_once($CFG->dirroot . '/mod/quiz/locallib.php'); + +/** + * Quiz attempt walk through. + * + * @package mod_quiz + * @category phpunit + * @copyright 2013 The Open University + * @author Jamie Pratt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_quiz_attempt_walkthrough_testcase extends advanced_testcase { + + /** + * Create a quiz with questions and walk through a quiz attempt. + */ + public function test_quiz_attempt_walkthrough() { + global $SITE; + + $this->resetAfterTest(true); + + // Make a quiz. + $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); + + $quiz = $quizgenerator->create_instance(array('course'=>$SITE->id, 'questionsperpage' => 0, 'grade' => 100.0, + 'sumgrades' => 2)); + + // Create a couple of questions. + $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); + + $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); + + // Make a user to do the quiz. + $user1 = $this->getDataGenerator()->create_user(); + $this->setUser($user1); + + $quizobj = quiz::create($quiz->id, $user1->id); + + // Start the attempt. + $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); + $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); + + $timenow = time(); + $attempt = quiz_create_attempt($quizobj, 1, false, $timenow); + + quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow); + + quiz_attempt_save_started($quizobj, $quba, $attempt); + + // Process some responses from the student. + $attemptobj = quiz_attempt::create($attempt->id); + $prefix1 = $quba->get_field_prefix(1); + $prefix2 = $quba->get_field_prefix(2); + + $tosubmit = array(); + $tosubmit['slots'] = '1,2'; + $tosubmit[$prefix1.':sequencecheck'] = 1; + $tosubmit[$prefix1.'answer'] = 'frog'; + + $tosubmit[$prefix2.':sequencecheck'] = 1; + $tosubmit[$prefix2.'answer'] = '3.14'; + + $attemptobj->process_submitted_actions($timenow, false, $tosubmit); + + // Finish the attempt. + $attemptobj = quiz_attempt::create($attempt->id); + $attemptobj->process_finish($timenow, false); + + // Re-load quiz attempt data. + $attemptobj = quiz_attempt::create($attempt->id); + + // Check that results are stored as expected. + $this->assertEquals(1, $attemptobj->get_attempt_number()); + $this->assertEquals(2, $attemptobj->get_sum_marks()); + $this->assertEquals(true, $attemptobj->is_finished()); + $this->assertEquals($timenow, $attemptobj->get_submitted_date()); + $this->assertEquals($user1->id, $attemptobj->get_userid()); + + // Check quiz grades. + $grades = quiz_get_user_grades($quiz, $user1->id); + $grade = array_shift($grades); + $this->assertEquals(100.0, $grade->rawgrade); + + // Check grade book. + $gradebookgrades = grade_get_grades($SITE->id, 'mod', 'quiz', $quiz->id, $user1->id); + $gradebookitem = array_shift($gradebookgrades->items); + $gradebookgrade = array_shift($gradebookitem->grades); + $this->assertEquals(100, $gradebookgrade->grade); + } +} diff --git a/mod/quiz/tests/attempts_test.php b/mod/quiz/tests/attempts_test.php index b70c26cffb7..901ca66c002 100644 --- a/mod/quiz/tests/attempts_test.php +++ b/mod/quiz/tests/attempts_test.php @@ -385,4 +385,4 @@ 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))); } -} \ No newline at end of file +}