// Update time now, in case the server is running really slowly. $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; }
protected function initialise_question_instance($question, $questiondata) { parent::initialise_question_instance($question, $questiondata); $bits = preg_split('/\\{#(\\d+)\\}/', $question->questiontext, null, PREG_SPLIT_DELIM_CAPTURE); $question->textfragments[0] = array_shift($bits); $i = 1; while (!empty($bits)) { $question->places[$i] = array_shift($bits); $question->textfragments[$i] = array_shift($bits); $i += 1; } foreach ($questiondata->options->questions as $key => $subqdata) { $subqdata->contextid = $questiondata->contextid; $question->subquestions[$key] = question_bank::make_question($subqdata); $question->subquestions[$key]->maxmark = $subqdata->defaultmark; if (isset($subqdata->options->layout)) { $question->subquestions[$key]->layout = $subqdata->options->layout; } } }
public function test_render_missing() { $questiondata = $this->get_unknown_questiondata(); $q = question_bank::make_question($questiondata); $qa = new testable_question_attempt($q, 0); $step = new question_attempt_step(array('answer' => 'frog')); $step->set_state(question_state::$todo); $qa->add_step($step); $qa->set_behaviour(new qbehaviour_deferredfeedback($qa, 'deferredfeedback')); $output = $qa->render(new question_display_options(), '1'); $this->assertRegExp('/' . preg_quote($qa->get_question()->questiontext, '/') . '/', $output); $this->assertRegExp('/' . preg_quote(get_string('missingqtypewarning', 'qtype_missingtype'), '/') . '/', $output); $this->assert(new question_contains_tag_with_attribute('div', 'class', 'warning missingqtypewarning'), $output); }
/** * Starts an attempt and returns the object representing it. * * @param int $quizid * @param int $userid * @throws moodle_exception * @return \stdClass */ public function start_quiz_attempt($quizid, $userid) { global $DB; $quizobj = quiz::create($quizid, $userid); $timenow = time(); $attempt = quiz_create_attempt($quizobj->get_quiz(), 1, false, $timenow, false); // Taken from /mod/quiz/startattempt.php. $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); // 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 = 1; } $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); question_engine::save_questions_usage_by_activity($quba); $attempt->uniqueid = $quba->get_id(); $attempt->id = $DB->insert_record('quiz_attempts', $attempt); 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 * @param array $questionids slot number => question id. Used for random questions, to force the choice * of a particular actual question. Intended for testing purposes only. * @param array $forcedvariantsbyslot slot number => variant. Used for questions with variants, * to force the choice of a particular variant. Intended for testing * purposes only. * @throws moodle_exception * @return object modified attempt object */ function quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $timenow, $questionids = array(), $forcedvariantsbyslot = array()) { // 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 { if (!isset($questionids[$quba->next_slot_number()])) { $forcequestionid = null; } else { $forcequestionid = $questionids[$quba->next_slot_number()]; } $question = question_bank::get_qtype('random')->choose_other_question( $questiondata, $questionsinuse, $quizobj->get_quiz()->shuffleanswers, $forcequestionid); 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; } $variantstrategy = new question_variant_pseudorandom_no_repeats_strategy( $variantoffset, $attempt->userid, $quizobj->get_quizid()); if (!empty($forcedvariantsbyslot)) { $forcedvariantsbyseed = question_variant_forced_choices_selection_strategy::prepare_forced_choices_array( $forcedvariantsbyslot, $quba); $variantstrategy = new question_variant_forced_choices_selection_strategy( $forcedvariantsbyseed, $variantstrategy); } $quba->start_all_questions($variantstrategy, $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 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 * @param array $questionids slot number => question id. Used for random questions, to force the choice * of a particular actual question. Intended for testing purposes only. * @param array $forcedvariantsbyslot slot number => variant. Used for questions with variants, * to force the choice of a particular variant. Intended for testing * purposes only. * @throws moodle_exception * @return object modified attempt object */ function quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $timenow, $questionids = array(), $forcedvariantsbyslot = array()) { // Usages for this user's previous quiz attempts. $qubaids = new \mod_quiz\question\qubaids_for_users_attempts($quizobj->get_quizid(), $attempt->userid); // Fully load all the questions in this quiz. $quizobj->preload_questions(); $quizobj->load_questions(); // First load all the non-random questions. $randomfound = false; $slot = 0; $questions = array(); $maxmark = array(); $page = array(); foreach ($quizobj->get_questions() as $questiondata) { $slot += 1; $maxmark[$slot] = $questiondata->maxmark; $page[$slot] = $questiondata->page; if ($questiondata->qtype == 'random') { $randomfound = true; continue; } if (!$quizobj->get_quiz()->shuffleanswers) { $questiondata->options->shuffleanswers = false; } $questions[$slot] = question_bank::make_question($questiondata); } // Then find a question to go in place of each random question. if ($randomfound) { $slot = 0; $usedquestionids = array(); foreach ($questions as $question) { if (isset($usedquestions[$question->id])) { $usedquestionids[$question->id] += 1; } else { $usedquestionids[$question->id] = 1; } } $randomloader = new \core_question\bank\random_question_loader($qubaids, $usedquestionids); foreach ($quizobj->get_questions() as $questiondata) { $slot += 1; if ($questiondata->qtype != 'random') { continue; } // Deal with fixed random choices for testing. if (isset($questionids[$quba->next_slot_number()])) { if ($randomloader->is_question_available($questiondata->category, (bool) $questiondata->questiontext, $questionids[$quba->next_slot_number()])) { $questions[$slot] = question_bank::load_question($questionids[$quba->next_slot_number()], $quizobj->get_quiz()->shuffleanswers); continue; } else { throw new coding_exception('Forced question id not available.'); } } // Normal case, pick one at random. $questionid = $randomloader->get_next_question_id($questiondata->category, (bool) $questiondata->questiontext); if ($questionid === null) { throw new moodle_exception('notenoughrandomquestions', 'quiz', $quizobj->view_url(), $questiondata); } $questions[$slot] = question_bank::load_question($questionid, $quizobj->get_quiz()->shuffleanswers); } } // Finally add them all to the usage. ksort($questions); foreach ($questions as $slot => $question) { $newslot = $quba->add_question($question, $maxmark[$slot]); if ($newslot != $slot) { throw new coding_exception('Slot numbers have got confused.'); } } // Start all the questions. $variantstrategy = new core_question\engine\variants\least_used_strategy($quba, $qubaids); if (!empty($forcedvariantsbyslot)) { $forcedvariantsbyseed = question_variant_forced_choices_selection_strategy::prepare_forced_choices_array($forcedvariantsbyslot, $quba); $variantstrategy = new question_variant_forced_choices_selection_strategy($forcedvariantsbyseed, $variantstrategy); } $quba->start_all_questions($variantstrategy, $timenow); // Work out the attempt layout. $sections = $quizobj->get_sections(); foreach ($sections as $i => $section) { if (isset($sections[$i + 1])) { $sections[$i]->lastslot = $sections[$i + 1]->firstslot - 1; } else { $sections[$i]->lastslot = count($questions); } } $layout = array(); foreach ($sections as $section) { if ($section->shufflequestions) { $questionsinthissection = array(); for ($slot = $section->firstslot; $slot <= $section->lastslot; $slot += 1) { $questionsinthissection[] = $slot; } shuffle($questionsinthissection); $questionsonthispage = 0; foreach ($questionsinthissection as $slot) { if ($questionsonthispage && $questionsonthispage == $quizobj->get_quiz()->questionsperpage) { $layout[] = 0; $questionsonthispage = 0; } $layout[] = $slot; $questionsonthispage += 1; } } else { $currentpage = $page[$section->firstslot]; for ($slot = $section->firstslot; $slot <= $section->lastslot; $slot += 1) { if ($currentpage !== null && $page[$slot] != $currentpage) { $layout[] = 0; } $layout[] = $slot; $currentpage = $page[$slot]; } } // Each section ends with a page break. $layout[] = 0; } $attempt->layout = implode(',', $layout); return $attempt; }
/** * Creates a single printable copy of the given quiz. * * @return The ID of the created printable question usage. * */ protected function create_printable_copy($shuffle_mode = quiz_papercopy_shuffle_modes::MODE_SHUFFLE_IGNORE_PAGES, $fix_descriptions = false, $fix_first = false, $fix_last = false) { //get a reference to the current user global $USER; //create a new usage object, which will allow us to create a psuedoquiz in the same context as the online quiz $usage = question_engine::make_questions_usage_by_activity('mod_quiz', $this->context); //and set the grading mode to "deferred feedback", the standard for paper quizzes //this makes sense, since our paradigm is duriven by the idea that feedback is only offered once a paper quiz has been uploaded/graded $usage->set_preferred_behaviour('deferredfeedback'); //get an array of questions in the current quiz $quiz_questions = $this->quizobj->get_questions(); //randomize the question order, as requested $quiz_questions = self::shuffle_questions($quiz_questions, $this->quiz->questions, $shuffle_mode, $fix_descriptions, $fix_first, $fix_last); //for each question in our online quiz foreach ($quiz_questions as $slot => $qdata) { $question = question_bank::make_question($qdata); //add the new question instance to our new printable copy, keeping the maximum grade from the quiz //TODO: respect maximum marks $usage->add_question($question); } //initialize each of the questions $usage->start_all_questions(); //save the usage to the database question_engine::save_questions_usage_by_activity($usage); //return the ID of the newly created questions usage return $usage->get_id(); }
/** * Retrieves a template question usage for an offline group. Creates a new template if there is none. * While creating question usage it shuffles the group questions if shuffleanswers is created. * * @param object $offlinequiz * @param object $group * @param object $context * @return question_usage_by_activity */ function offlinequiz_get_group_template_usage($offlinequiz, $group, $context) { global $CFG, $DB; if (!empty($group->templateusageid) && $group->templateusageid > 0) { $templateusage = question_engine::load_questions_usage_by_activity($group->templateusageid); } else { $questionids = offlinequiz_get_group_question_ids($offlinequiz, $group->id); if ($offlinequiz->shufflequestions) { $offlinequiz->groupid = $group->id; $questionids = offlinequiz_shuffle_questions($questionids); } // We have to use our own class s.t. we can use the clone function to create results. $templateusage = offlinequiz_make_questions_usage_by_activity('mod_offlinequiz', $context); $templateusage->set_preferred_behaviour('immediatefeedback'); if (!$questionids) { print_error(get_string('noquestionsfound', 'offlinequiz'), 'view.php?q=' . $offlinequiz->id); } // Gets database raw data for the questions. $questiondata = question_load_questions($questionids); // Get the question instances for initial markmarks. $sql = "SELECT questionid, maxmark\n FROM {offlinequiz_group_questions}\n WHERE offlinequizid = :offlinequizid\n AND offlinegroupid = :offlinegroupid "; $groupquestions = $DB->get_records_sql($sql, array('offlinequizid' => $offlinequiz->id, 'offlinegroupid' => $group->id)); foreach ($questionids as $questionid) { if ($questionid) { // Convert the raw data of multichoice questions to a real question definition object. if (!$offlinequiz->shuffleanswers) { $questiondata[$questionid]->options->shuffleanswers = false; } $question = question_bank::make_question($questiondata[$questionid]); // We only add multichoice questions which are needed for grading. if ($question->get_type_name() == 'multichoice' || $question->get_type_name() == 'multichoiceset') { $templateusage->add_question($question, $groupquestions[$question->id]->maxmark); } } } // Create attempts for all questions (fixes order of the answers if shuffleanswers is active). $templateusage->start_all_questions(); // Save the template question usage to the DB. question_engine::save_questions_usage_by_activity($templateusage); // Save the templateusage-ID in the offlinequiz_groups table. $group->templateusageid = $templateusage->get_id(); $DB->set_field('offlinequiz_groups', 'templateusageid', $group->templateusageid, array('id' => $group->id)); } // End else. return $templateusage; }
/** * add the questions to the question usage * This is called by the question_attmept class on construct of a new attempt * * @param \question_usage_by_activity $quba * * @return array */ public function add_questions_to_quba(\question_usage_by_activity $quba) { // we need the questionids of our questions $questionids = array(); foreach ($this->qbankOrderedQuestions as $qbankquestion) { /** @var activequiz_question $qbankquestion */ if (!in_array($qbankquestion->getQuestion()->id, $questionids)) { $questionids[] = $qbankquestion->getQuestion()->id; } } $questions = question_load_questions($questionids); // loop through the ordered question bank questions and add them to the quba // object $attemptlayout = array(); foreach ($this->qbankOrderedQuestions as $qbankquestion) { $questionid = $qbankquestion->getQuestion()->id; $q = \question_bank::make_question($questions[$questionid]); $attemptlayout[$qbankquestion->getId()] = $quba->add_question($q, $qbankquestion->getPoints()); } // start the questions in the quba $quba->start_all_questions(); /** * return the attempt layout which is a set of ids that are the slot ids from the question engine usage by activity instance * these are what are used during an actual attempt rather than the questionid themselves, since the question engine will handle * the translation */ return $attemptlayout; }
private static function quiz_question_loop($attempt, $quizobj, $quba, $questionsinuse) { foreach ($quizobj->get_questions() as $i => $questiondata) { $questiondata->options->shuffleanswers = 0; if ($questiondata->qtype != 'random') { $question = question_bank::make_question($questiondata); } $idstoslots[$i] = $quba->add_question($question, $questiondata->maxmark); $questionsinuse[] = $question->id; } return $idstoslots; }