mirror of
https://github.com/moodle/moodle.git
synced 2025-04-16 14:02:32 +02:00
Merge branch 'MDL-6340' of git://github.com/timhunt/moodle
This commit is contained in:
commit
b51ff393cd
74
mod/quiz/classes/question/qubaids_for_users_attempts.php
Normal file
74
mod/quiz/classes/question/qubaids_for_users_attempts.php
Normal file
@ -0,0 +1,74 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* A {@link qubaid_condition} representing all the attempts by one user at a given quiz.
|
||||
*
|
||||
* @package mod_quiz
|
||||
* @category question
|
||||
* @copyright 2015 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace mod_quiz\question;
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
|
||||
/**
|
||||
* A {@link qubaid_condition} representing all the attempts by one user at a given quiz.
|
||||
*
|
||||
* @copyright 2015 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class qubaids_for_users_attempts extends \qubaid_join {
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* This takes the same arguments as {@link quiz_get_user_attempts()}.
|
||||
*
|
||||
* @param int $quizid the quiz id.
|
||||
* @param int $userid the userid.
|
||||
* @param string $status 'all', 'finished' or 'unfinished' to control
|
||||
* @param bool $includepreviews defaults to false.
|
||||
*/
|
||||
public function __construct($quizid, $userid, $status = 'finished', $includepreviews = false) {
|
||||
$where = 'quiza.quiz = :quizaquiz AND quiza.userid = :userid';
|
||||
$params = array('quizaquiz' => $quizid, 'userid' => $userid);
|
||||
|
||||
if (!$includepreviews) {
|
||||
$where .= ' AND preview = 0';
|
||||
}
|
||||
|
||||
switch ($status) {
|
||||
case 'all':
|
||||
break;
|
||||
|
||||
case 'finished':
|
||||
$where .= ' AND state IN (:state1, :state2)';
|
||||
$params['state1'] = \quiz_attempt::FINISHED;
|
||||
$params['state2'] = \quiz_attempt::ABANDONED;
|
||||
break;
|
||||
|
||||
case 'unfinished':
|
||||
$where .= ' AND state IN (:state1, :state2)';
|
||||
$params['state1'] = \quiz_attempt::IN_PROGRESS;
|
||||
$params['state2'] = \quiz_attempt::OVERDUE;
|
||||
break;
|
||||
}
|
||||
|
||||
parent::__construct('{quiz_attempts} quiza', 'quiza.uniqueid', $where, $params);
|
||||
}
|
||||
}
|
@ -158,46 +158,88 @@ function quiz_create_attempt(quiz $quizobj, $attemptnumber, $lastattempt, $timen
|
||||
*/
|
||||
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();
|
||||
|
||||
// Add them all to the $quba.
|
||||
$questionsinuse = array_keys($quizobj->get_questions());
|
||||
// First load all the non-random questions.
|
||||
$randomfound = false;
|
||||
$slot = 0;
|
||||
$questions = array();
|
||||
$maxmark = array();
|
||||
foreach ($quizobj->get_questions() as $questiondata) {
|
||||
if ($questiondata->qtype != 'random') {
|
||||
if (!$quizobj->get_quiz()->shuffleanswers) {
|
||||
$questiondata->options->shuffleanswers = false;
|
||||
}
|
||||
$question = question_bank::make_question($questiondata);
|
||||
$slot += 1;
|
||||
$maxmark[$slot] = $questiondata->maxmark;
|
||||
if ($questiondata->qtype == 'random') {
|
||||
$randomfound = true;
|
||||
continue;
|
||||
}
|
||||
if (!$quizobj->get_quiz()->shuffleanswers) {
|
||||
$questiondata->options->shuffleanswers = false;
|
||||
}
|
||||
$questions[$slot] = question_bank::make_question($questiondata);
|
||||
}
|
||||
|
||||
} else {
|
||||
if (!isset($questionids[$quba->next_slot_number()])) {
|
||||
$forcequestionid = null;
|
||||
// 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 {
|
||||
$forcequestionid = $questionids[$quba->next_slot_number()];
|
||||
$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;
|
||||
}
|
||||
|
||||
$question = question_bank::get_qtype('random')->choose_other_question(
|
||||
$questiondata, $questionsinuse, $quizobj->get_quiz()->shuffleanswers, $forcequestionid);
|
||||
if (is_null($question)) {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
$quba->add_question($question, $questiondata->maxmark);
|
||||
$questionsinuse[] = $question->id;
|
||||
$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.
|
||||
if ($attempt->preview) {
|
||||
$variantoffset = rand(1, 100);
|
||||
} else {
|
||||
$variantoffset = $attemptnumber;
|
||||
}
|
||||
$variantstrategy = new question_variant_pseudorandom_no_repeats_strategy(
|
||||
$variantoffset, $attempt->userid, $quizobj->get_quizid());
|
||||
$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(
|
||||
|
@ -205,6 +205,9 @@ class mod_quiz_lib_testcase extends advanced_testcase {
|
||||
$attemptobj->process_finish($timenow, false);
|
||||
|
||||
// Start the failing 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, false, $failstudent->id);
|
||||
quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
|
||||
|
223
question/classes/bank/random_question_loader.php
Normal file
223
question/classes/bank/random_question_loader.php
Normal file
@ -0,0 +1,223 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* A class for efficiently finds questions at random from the question bank.
|
||||
*
|
||||
* @package core_question
|
||||
* @copyright 2015 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace core_question\bank;
|
||||
|
||||
|
||||
/**
|
||||
* This class efficiently finds questions at random from the question bank.
|
||||
*
|
||||
* You can ask for questions at random one at a time. Each time you ask, you
|
||||
* pass a category id, and whether to pick from that category and all subcategories
|
||||
* or just that category.
|
||||
*
|
||||
* The number of teams each question has been used is tracked, and we will always
|
||||
* return a question from among those elegible that has been used the fewest times.
|
||||
* So, if there are questions that have not been used yet in the category asked for,
|
||||
* one of those will be returned. However, within one instantiation of this class,
|
||||
* we will never return a given question more than once, and we will never return
|
||||
* questions passed into the constructor as $usedquestions.
|
||||
*
|
||||
* @copyright 2015 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class random_question_loader {
|
||||
/** @var \qubaid_condition which usages to consider previous attempts from. */
|
||||
protected $qubaids;
|
||||
|
||||
/** @var array qtypes that cannot be used by random questions. */
|
||||
protected $excludedqtypes;
|
||||
|
||||
/** @var array categoryid & include subcategories => num previous uses => questionid => 1. */
|
||||
protected $availablequestionscache = array();
|
||||
|
||||
/**
|
||||
* @var array questionid => num recent uses. Questions that have been used,
|
||||
* but that is not yet recorded in the DB.
|
||||
*/
|
||||
protected $recentlyusedquestions;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param \qubaid_condition $qubaids the usages to consider when counting previous uses of each question.
|
||||
* @param array $usedquestions questionid => number of times used count. If we should allow for
|
||||
* further existing uses of a question in addition to the ones in $qubaids.
|
||||
*/
|
||||
public function __construct(\qubaid_condition $qubaids, array $usedquestions = array()) {
|
||||
$this->qubaids = $qubaids;
|
||||
$this->recentlyusedquestions = $usedquestions;
|
||||
|
||||
foreach (\question_bank::get_all_qtypes() as $qtype) {
|
||||
if (!$qtype->is_usable_by_random()) {
|
||||
$this->excludedqtypes[] = $qtype->name();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick a question at random from the given category, from among those with the fewest uses.
|
||||
*
|
||||
* It is up the the caller to verify that the cateogry exists. An unknown category
|
||||
* behaves like an empty one.
|
||||
*
|
||||
* @param int $categoryid the id of a category in the question bank.
|
||||
* @param bool $includesubcategories wether to pick a question from exactly
|
||||
* that category, or that category and subcategories.
|
||||
* @return int|null the id of the question picked, or null if there aren't any.
|
||||
*/
|
||||
public function get_next_question_id($categoryid, $includesubcategories) {
|
||||
$this->ensure_questions_for_category_loaded($categoryid, $includesubcategories);
|
||||
|
||||
$categorykey = $this->get_category_key($categoryid, $includesubcategories);
|
||||
if (empty($this->availablequestionscache[$categorykey])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
reset($this->availablequestionscache[$categorykey]);
|
||||
$lowestcount = key($this->availablequestionscache[$categorykey]);
|
||||
reset($this->availablequestionscache[$categorykey][$lowestcount]);
|
||||
$questionid = key($this->availablequestionscache[$categorykey][$lowestcount]);
|
||||
$this->use_question($questionid);
|
||||
return $questionid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the key into {@link $availablequestionscache} for this combination of options.
|
||||
* @param int $categoryid the id of a category in the question bank.
|
||||
* @param bool $includesubcategories wether to pick a question from exactly
|
||||
* that category, or that category and subcategories.
|
||||
* @return string the cache key.
|
||||
*/
|
||||
protected function get_category_key($categoryid, $includesubcategories) {
|
||||
if ($includesubcategories) {
|
||||
return $categoryid . '|1';
|
||||
} else {
|
||||
return $categoryid . '|0';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate {@link $availablequestionscache} for this combination of options.
|
||||
* @param int $categoryid the id of a category in the question bank.
|
||||
* @param bool $includesubcategories wether to pick a question from exactly
|
||||
* that category, or that category and subcategories.
|
||||
*/
|
||||
protected function ensure_questions_for_category_loaded($categoryid, $includesubcategories) {
|
||||
global $DB;
|
||||
|
||||
$categorykey = $this->get_category_key($categoryid, $includesubcategories);
|
||||
|
||||
if (isset($this->availablequestionscache[$categorykey])) {
|
||||
// Data is already in the cache, nothing to do.
|
||||
return;
|
||||
}
|
||||
|
||||
// Load the available questions from the question bank.
|
||||
if ($includesubcategories) {
|
||||
$categoryids = question_categorylist($categoryid);
|
||||
} else {
|
||||
$categoryids = array($categoryid);
|
||||
}
|
||||
|
||||
list($extraconditions, $extraparams) = $DB->get_in_or_equal($this->excludedqtypes,
|
||||
SQL_PARAMS_NAMED, 'excludedqtype', false);
|
||||
|
||||
$questionidsandcounts = \question_bank::get_finder()->get_questions_from_categories_with_usage_counts(
|
||||
$categoryids, $this->qubaids, 'q.qtype ' . $extraconditions, $extraparams);
|
||||
if (!$questionidsandcounts) {
|
||||
// No questions in this category.
|
||||
$this->availablequestionscache[$categorykey] = array();
|
||||
return;
|
||||
}
|
||||
|
||||
// Put all the questions with each value of $prevusecount in separate arrays.
|
||||
$idsbyusecount = array();
|
||||
foreach ($questionidsandcounts as $questionid => $prevusecount) {
|
||||
if (isset($this->recentlyusedquestions[$questionid])) {
|
||||
// Recently used questions are never returned.
|
||||
continue;
|
||||
}
|
||||
$idsbyusecount[$prevusecount][] = $questionid;
|
||||
}
|
||||
|
||||
// Now put that data into our cache. For each count, we need to shuffle
|
||||
// questionids, and make those the keys of an array.
|
||||
$this->availablequestionscache[$categorykey] = array();
|
||||
foreach ($idsbyusecount as $prevusecount => $questionids) {
|
||||
shuffle($questionids);
|
||||
$this->availablequestionscache[$categorykey][$prevusecount] = array_combine(
|
||||
$questionids, array_fill(0, count($questionids), 1));
|
||||
}
|
||||
ksort($this->availablequestionscache[$categorykey]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the internal data structures to indicate that a given question has
|
||||
* been used one more time.
|
||||
*
|
||||
* @param int $questionid the question that is being used.
|
||||
*/
|
||||
protected function use_question($questionid) {
|
||||
if (isset($this->recentlyusedquestions[$questionid])) {
|
||||
$this->recentlyusedquestions[$questionid] += 1;
|
||||
} else {
|
||||
$this->recentlyusedquestions[$questionid] = 1;
|
||||
}
|
||||
|
||||
foreach ($this->availablequestionscache as $categorykey => $questionsforcategory) {
|
||||
foreach ($questionsforcategory as $numuses => $questionids) {
|
||||
if (!isset($questionids[$questionid])) {
|
||||
continue;
|
||||
}
|
||||
unset($this->availablequestionscache[$categorykey][$numuses][$questionid]);
|
||||
if (empty($this->availablequestionscache[$categorykey][$numuses])) {
|
||||
unset($this->availablequestionscache[$categorykey][$numuses]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a given question is available in a given category. If so, mark it used.
|
||||
*
|
||||
* @param int $categoryid the id of a category in the question bank.
|
||||
* @param bool $includesubcategories wether to pick a question from exactly
|
||||
* that category, or that category and subcategories.
|
||||
* @param int $questionid the question that is being used.
|
||||
* @return bool whether the question is available in the requested category.
|
||||
*/
|
||||
public function is_question_available($categoryid, $includesubcategories, $questionid) {
|
||||
$this->ensure_questions_for_category_loaded($categoryid, $includesubcategories);
|
||||
$categorykey = $this->get_category_key($categoryid, $includesubcategories);
|
||||
|
||||
foreach ($this->availablequestionscache[$categorykey] as $questionids) {
|
||||
if (isset($questionids[$questionid])) {
|
||||
$this->use_question($questionid);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
128
question/classes/engine/variants/least_used_strategy.php
Normal file
128
question/classes/engine/variants/least_used_strategy.php
Normal file
@ -0,0 +1,128 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* A {@link \question_variant_selection_strategy} that randomly selects variants that were not used yet.
|
||||
*
|
||||
* @package core_question
|
||||
* @copyright 2015 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
|
||||
namespace core_question\engine\variants;
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
|
||||
/**
|
||||
* A {@link \question_variant_selection_strategy} that randomly selects variants that were not used yet.
|
||||
*
|
||||
* If all variants have been used at least once in the set of usages under
|
||||
* consideration, then then it picks one of the least often used.
|
||||
*
|
||||
* Within one particular use of this class, each seed will always select the
|
||||
* same variant. This is so that shared datasets work in calculated questions,
|
||||
* and similar features in question types like varnumeric and STACK.
|
||||
*
|
||||
* @copyright 2015 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class least_used_strategy implements \question_variant_selection_strategy {
|
||||
|
||||
/** @var array seed => variant number => number of uses. */
|
||||
protected $variantsusecounts = array();
|
||||
|
||||
/** @var array seed => variant number. */
|
||||
protected $selectedvariant = array();
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param question_usage_by_activity $quba the question usage we will be picking variants for.
|
||||
* @param qubaid_condition $qubaids ids of the usages to consider when counting previous uses of each variant.
|
||||
*/
|
||||
public function __construct(\question_usage_by_activity $quba, \qubaid_condition $qubaids) {
|
||||
$questionidtoseed = array();
|
||||
foreach ($quba->get_attempt_iterator() as $qa) {
|
||||
$question = $qa->get_question();
|
||||
if ($question->get_num_variants() > 1) {
|
||||
$questionidtoseed[$question->id] = $question->get_variants_selection_seed();
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($questionidtoseed)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->variantsusecounts = array_fill_keys($questionidtoseed, array());
|
||||
|
||||
$variantsused = \question_engine::load_used_variants(array_keys($questionidtoseed), $qubaids);
|
||||
foreach ($variantsused as $questionid => $usagecounts) {
|
||||
$seed = $questionidtoseed[$questionid];
|
||||
foreach ($usagecounts as $variant => $count) {
|
||||
if (isset($this->variantsusecounts[$seed][$variant])) {
|
||||
$this->variantsusecounts[$seed][$variant] += $count;
|
||||
} else {
|
||||
$this->variantsusecounts[$seed][$variant] = $count;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function choose_variant($maxvariants, $seed) {
|
||||
if ($maxvariants == 1) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (isset($this->selectedvariant[$seed])) {
|
||||
return $this->selectedvariant[$seed];
|
||||
}
|
||||
|
||||
if ($maxvariants > 2 * count($this->variantsusecounts[$seed])) {
|
||||
// Many many more variants exist than have been used so far.
|
||||
// It will be quicker to just pick until we miss a collision.
|
||||
do {
|
||||
$variant = rand(1, $maxvariants);
|
||||
} while (isset($this->variantsusecounts[$seed][$variant]));
|
||||
|
||||
} else {
|
||||
// We need to work harder to find a least-used one.
|
||||
$leastusedvariants = array();
|
||||
for ($variant = 1; $variant <= $maxvariants; ++$variant) {
|
||||
if (!isset($this->variantsusecounts[$seed][$variant])) {
|
||||
$leastusedvariants[$variant] = 1;
|
||||
}
|
||||
}
|
||||
if (empty($leastusedvariants)) {
|
||||
// All variants used at least once, try again.
|
||||
$leastuses = min($this->variantsusecounts[$seed]);
|
||||
foreach ($this->variantsusecounts[$seed] as $variant => $uses) {
|
||||
if ($uses == $leastuses) {
|
||||
$leastusedvariants[$variant] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
$variant = array_rand($leastusedvariants);
|
||||
}
|
||||
|
||||
$this->selectedvariant[$seed] = $variant;
|
||||
if (isset($variantsusecounts[$seed][$variant])) {
|
||||
$variantsusecounts[$seed][$variant] += 1;
|
||||
} else {
|
||||
$variantsusecounts[$seed][$variant] = 1;
|
||||
}
|
||||
return $variant;
|
||||
}
|
||||
}
|
@ -480,7 +480,7 @@ class question_finder implements cache_data_source {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ids of all the questions in a list of categoryies.
|
||||
* Get the ids of all the questions in a list of categories.
|
||||
* @param array $categoryids either a categoryid, or a comma-separated list
|
||||
* category ids, or an array of them.
|
||||
* @param string $extraconditions extra conditions to AND with the rest of
|
||||
@ -505,6 +505,43 @@ class question_finder implements cache_data_source {
|
||||
{$extraconditions}", $qcparams + $extraparams, '', 'id,id AS id2');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ids of all the questions in a list of categories, with the number
|
||||
* of times they have already been used in a given set of usages.
|
||||
*
|
||||
* The result array is returned in order of increasing (count previous uses).
|
||||
*
|
||||
* @param array $categoryids an array question_category ids.
|
||||
* @param qubaid_condition $qubaids which question_usages to count previous uses from.
|
||||
* @param string $extraconditions extra conditions to AND with the rest of
|
||||
* the where clause. Must use named parameters.
|
||||
* @param array $extraparams any parameters used by $extraconditions.
|
||||
* @return array questionid => count of number of previous uses.
|
||||
*/
|
||||
public function get_questions_from_categories_with_usage_counts($categoryids,
|
||||
qubaid_condition $qubaids, $extraconditions = '', $extraparams = array()) {
|
||||
global $DB;
|
||||
|
||||
list($qcsql, $qcparams) = $DB->get_in_or_equal($categoryids, SQL_PARAMS_NAMED, 'qc');
|
||||
|
||||
if ($extraconditions) {
|
||||
$extraconditions = ' AND (' . $extraconditions . ')';
|
||||
}
|
||||
|
||||
return $DB->get_records_sql_menu("
|
||||
SELECT q.id, (SELECT COUNT(1)
|
||||
FROM " . $qubaids->from_question_attempts('qa') . "
|
||||
WHERE qa.questionid = q.id AND " . $qubaids->where() . "
|
||||
) AS previous_attempts
|
||||
|
||||
FROM {question} q
|
||||
|
||||
WHERE q.category $qcsql $extraconditions
|
||||
|
||||
ORDER BY previous_attempts
|
||||
", $qubaids->from_where_params() + $qcparams + $extraparams);
|
||||
}
|
||||
|
||||
/* See cache_data_source::load_for_cache. */
|
||||
public function load_for_cache($questionid) {
|
||||
global $DB;
|
||||
|
@ -1198,6 +1198,32 @@ ORDER BY
|
||||
'questionid ' . $test . ' AND questionusageid ' .
|
||||
$qubaids->usage_id_in(), $params + $qubaids->usage_id_in_params());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of times each variant has been used for each question in a list
|
||||
* in a set of usages.
|
||||
* @param array $questionids of question ids.
|
||||
* @param qubaid_condition $qubaids ids of the usages to consider.
|
||||
* @return array questionid => variant number => num uses.
|
||||
*/
|
||||
public function load_used_variants(array $questionids, qubaid_condition $qubaids) {
|
||||
list($test, $params) = $this->db->get_in_or_equal($questionids, SQL_PARAMS_NAMED, 'qid');
|
||||
$recordset = $this->db->get_recordset_sql("
|
||||
SELECT qa.questionid, qa.variant, COUNT(1) AS usescount
|
||||
FROM " . $qubaids->from_question_attempts('qa') . "
|
||||
WHERE qa.questionid $test
|
||||
AND " . $qubaids->where() . "
|
||||
GROUP BY qa.questionid, qa.variant
|
||||
ORDER BY COUNT(1) ASC
|
||||
", $params + $qubaids->from_where_params());
|
||||
|
||||
$usedvariants = array_combine($questionids, array_fill(0, count($questionids), array()));
|
||||
foreach ($recordset as $row) {
|
||||
$usedvariants[$row->questionid][$row->variant] = $row->usescount;
|
||||
}
|
||||
$recordset->close();
|
||||
return $usedvariants;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -160,6 +160,18 @@ abstract class question_engine {
|
||||
return $dm->questions_in_use($questionids, $qubaids);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of times each variant has been used for each question in a list
|
||||
* in a set of usages.
|
||||
* @param array $questionids of question ids.
|
||||
* @param qubaid_condition $qubaids ids of the usages to consider.
|
||||
* @return array questionid => variant number => num uses.
|
||||
*/
|
||||
public static function load_used_variants(array $questionids, qubaid_condition $qubaids) {
|
||||
$dm = new question_engine_data_mapper();
|
||||
return $dm->load_used_variants($questionids, $qubaids);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an archetypal behaviour for a particular question attempt.
|
||||
* Used by {@link question_definition::make_behaviour()}.
|
||||
|
@ -486,14 +486,15 @@ class question_usage_by_activity {
|
||||
* @param int $variant which variant of the question to use. Must be between
|
||||
* 1 and ->get_num_variants($slot) inclusive. If not give, a variant is
|
||||
* chosen at random.
|
||||
* @param int $timestamp optional, the timstamp to record for this action. Defaults to now.
|
||||
*/
|
||||
public function start_question($slot, $variant = null) {
|
||||
public function start_question($slot, $variant = null, $timenow = null) {
|
||||
if (is_null($variant)) {
|
||||
$variant = rand(1, $this->get_num_variants($slot));
|
||||
}
|
||||
|
||||
$qa = $this->get_question_attempt($slot);
|
||||
$qa->start($this->preferredbehaviour, $variant);
|
||||
$qa->start($this->preferredbehaviour, $variant, array(), $timenow);
|
||||
$this->observer->notify_attempt_modified($qa);
|
||||
}
|
||||
|
||||
|
@ -126,4 +126,32 @@ class question_engine_data_mapper_testcase extends qbehaviour_walkthrough_test_b
|
||||
$this->assertEquals( 5, $quba2->get_question_max_mark(1));
|
||||
$this->assertEquals( 2, $quba2->get_question_max_mark(2));
|
||||
}
|
||||
|
||||
public function test_load_used_variants() {
|
||||
$this->resetAfterTest();
|
||||
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
|
||||
$cat = $generator->create_question_category();
|
||||
$questiondata1 = $generator->create_question('shortanswer', null, array('category' => $cat->id));
|
||||
$questiondata2 = $generator->create_question('shortanswer', null, array('category' => $cat->id));
|
||||
$questiondata3 = $generator->create_question('shortanswer', null, array('category' => $cat->id));
|
||||
|
||||
$quba = question_engine::make_questions_usage_by_activity('test', context_system::instance());
|
||||
$quba->set_preferred_behaviour('deferredfeedback');
|
||||
$question1 = question_bank::load_question($questiondata1->id);
|
||||
$question3 = question_bank::load_question($questiondata3->id);
|
||||
$quba->add_question($question1);
|
||||
$quba->add_question($question1);
|
||||
$quba->add_question($question3);
|
||||
$quba->start_all_questions();
|
||||
question_engine::save_questions_usage_by_activity($quba);
|
||||
|
||||
$this->assertEquals(array(
|
||||
$questiondata1->id => array(1 => 2),
|
||||
$questiondata2->id => array(),
|
||||
$questiondata3->id => array(1 => 1),
|
||||
), question_engine::load_used_variants(
|
||||
array($questiondata1->id, $questiondata2->id, $questiondata3->id),
|
||||
new qubaid_list(array($quba->get_id()))));
|
||||
}
|
||||
}
|
||||
|
@ -83,4 +83,31 @@ class question_bank_test extends advanced_testcase {
|
||||
$this->assertSame('-83.33333%', end($fractions));
|
||||
$this->assertSame('-0.8333333', key($fractions));
|
||||
}
|
||||
|
||||
public function test_get_questions_from_categories_with_usage_counts() {
|
||||
$this->resetAfterTest();
|
||||
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
|
||||
$cat = $generator->create_question_category();
|
||||
$questiondata1 = $generator->create_question('shortanswer', null, array('category' => $cat->id));
|
||||
$questiondata2 = $generator->create_question('shortanswer', null, array('category' => $cat->id));
|
||||
$questiondata3 = $generator->create_question('shortanswer', null, array('category' => $cat->id));
|
||||
|
||||
$quba = question_engine::make_questions_usage_by_activity('test', context_system::instance());
|
||||
$quba->set_preferred_behaviour('deferredfeedback');
|
||||
$question1 = question_bank::load_question($questiondata1->id);
|
||||
$question3 = question_bank::load_question($questiondata3->id);
|
||||
$quba->add_question($question1);
|
||||
$quba->add_question($question1);
|
||||
$quba->add_question($question3);
|
||||
$quba->start_all_questions();
|
||||
question_engine::save_questions_usage_by_activity($quba);
|
||||
|
||||
$this->assertEquals(array(
|
||||
$questiondata2->id => 0,
|
||||
$questiondata3->id => 1,
|
||||
$questiondata1->id => 2,
|
||||
), question_bank::get_finder()->get_questions_from_categories_with_usage_counts(
|
||||
array($cat->id), new qubaid_list(array($quba->get_id()))));
|
||||
}
|
||||
}
|
||||
|
153
question/engine/tests/questionusagebyactivity_data_test.php
Normal file
153
question/engine/tests/questionusagebyactivity_data_test.php
Normal file
@ -0,0 +1,153 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* This file contains tests for the question_usage_by_activity class.
|
||||
*
|
||||
* @package core_question
|
||||
* @copyright 2012 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
global $CFG;
|
||||
require_once(dirname(__FILE__) . '/../lib.php');
|
||||
require_once(dirname(__FILE__) . '/helpers.php');
|
||||
|
||||
|
||||
/**
|
||||
* Unit tests for loading data into the {@link question_usage_by_activity} class.
|
||||
*
|
||||
* @copyright 2012 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class question_usage_db_test extends data_loading_method_test_base {
|
||||
public function test_load() {
|
||||
$scid = context_system::instance()->id;
|
||||
$records = new question_test_recordset(array(
|
||||
array('qubaid', 'contextid', 'component', 'preferredbehaviour',
|
||||
'questionattemptid', 'questionusageid', 'slot',
|
||||
'behaviour', 'questionid', 'variant', 'maxmark', 'minfraction', 'maxfraction', 'flagged',
|
||||
'questionsummary', 'rightanswer', 'responsesummary', 'timemodified',
|
||||
'attemptstepid', 'sequencenumber', 'state', 'fraction',
|
||||
'timecreated', 'userid', 'name', 'value'),
|
||||
array(1, $scid, 'unit_test', 'interactive', 1, 1, 1, 'interactive', -1, 1, 2.0000000, 0.0000000, 1.0000000, 0, '', '', '', 1256233790, 1, 0, 'todo', null, 1256233700, 1, null, null),
|
||||
array(1, $scid, 'unit_test', 'interactive', 1, 1, 1, 'interactive', -1, 1, 2.0000000, 0.0000000, 1.0000000, 0, '', '', '', 1256233790, 2, 1, 'todo', null, 1256233705, 1, 'answer', '1'),
|
||||
array(1, $scid, 'unit_test', 'interactive', 1, 1, 1, 'interactive', -1, 1, 2.0000000, 0.0000000, 1.0000000, 0, '', '', '', 1256233790, 5, 2, 'gradedright', 1.0000000, 1256233720, 1, '-finish', '1'),
|
||||
));
|
||||
|
||||
$question = test_question_maker::make_question('truefalse', 'true');
|
||||
$question->id = -1;
|
||||
|
||||
question_bank::start_unit_test();
|
||||
question_bank::load_test_question_data($question);
|
||||
$quba = question_usage_by_activity::load_from_records($records, 1);
|
||||
question_bank::end_unit_test();
|
||||
|
||||
$this->assertEquals('unit_test', $quba->get_owning_component());
|
||||
$this->assertEquals(1, $quba->get_id());
|
||||
$this->assertInstanceOf('question_engine_unit_of_work', $quba->get_observer());
|
||||
$this->assertEquals('interactive', $quba->get_preferred_behaviour());
|
||||
|
||||
$qa = $quba->get_question_attempt(1);
|
||||
|
||||
$this->assertEquals($question->questiontext, $qa->get_question()->questiontext);
|
||||
|
||||
$this->assertEquals(3, $qa->get_num_steps());
|
||||
|
||||
$step = $qa->get_step(0);
|
||||
$this->assertEquals(question_state::$todo, $step->get_state());
|
||||
$this->assertNull($step->get_fraction());
|
||||
$this->assertEquals(1256233700, $step->get_timecreated());
|
||||
$this->assertEquals(1, $step->get_user_id());
|
||||
$this->assertEquals(array(), $step->get_all_data());
|
||||
|
||||
$step = $qa->get_step(1);
|
||||
$this->assertEquals(question_state::$todo, $step->get_state());
|
||||
$this->assertNull($step->get_fraction());
|
||||
$this->assertEquals(1256233705, $step->get_timecreated());
|
||||
$this->assertEquals(1, $step->get_user_id());
|
||||
$this->assertEquals(array('answer' => '1'), $step->get_all_data());
|
||||
|
||||
$step = $qa->get_step(2);
|
||||
$this->assertEquals(question_state::$gradedright, $step->get_state());
|
||||
$this->assertEquals(1, $step->get_fraction());
|
||||
$this->assertEquals(1256233720, $step->get_timecreated());
|
||||
$this->assertEquals(1, $step->get_user_id());
|
||||
$this->assertEquals(array('-finish' => '1'), $step->get_all_data());
|
||||
}
|
||||
|
||||
public function test_load_data_no_steps() {
|
||||
// The code had a bug where if one question_attempt had no steps,
|
||||
// load_from_records got stuck in an infinite loop. This test is to
|
||||
// verify that no longer happens.
|
||||
$scid = context_system::instance()->id;
|
||||
$records = new question_test_recordset(array(
|
||||
array('qubaid', 'contextid', 'component', 'preferredbehaviour',
|
||||
'questionattemptid', 'questionusageid', 'slot',
|
||||
'behaviour', 'questionid', 'variant', 'maxmark', 'minfraction', 'maxfraction', 'flagged',
|
||||
'questionsummary', 'rightanswer', 'responsesummary', 'timemodified',
|
||||
'attemptstepid', 'sequencenumber', 'state', 'fraction',
|
||||
'timecreated', 'userid', 'name', 'value'),
|
||||
array(1, $scid, 'unit_test', 'interactive', 1, 1, 1, 'interactive', 0, 1, 1.0000000, 0.0000000, 1.0000000, 0, 'This question is missing. Unable to display anything.', '', '', 0, null, null, null, null, null, null, null, null),
|
||||
array(1, $scid, 'unit_test', 'interactive', 2, 1, 2, 'interactive', 0, 1, 1.0000000, 0.0000000, 1.0000000, 0, 'This question is missing. Unable to display anything.', '', '', 0, null, null, null, null, null, null, null, null),
|
||||
array(1, $scid, 'unit_test', 'interactive', 3, 1, 3, 'interactive', 0, 1, 1.0000000, 0.0000000, 1.0000000, 0, 'This question is missing. Unable to display anything.', '', '', 0, null, null, null, null, null, null, null, null),
|
||||
));
|
||||
|
||||
question_bank::start_unit_test();
|
||||
$quba = question_usage_by_activity::load_from_records($records, 1);
|
||||
question_bank::end_unit_test();
|
||||
|
||||
$this->assertEquals('unit_test', $quba->get_owning_component());
|
||||
$this->assertEquals(1, $quba->get_id());
|
||||
$this->assertInstanceOf('question_engine_unit_of_work', $quba->get_observer());
|
||||
$this->assertEquals('interactive', $quba->get_preferred_behaviour());
|
||||
|
||||
$this->assertEquals(array(1, 2, 3), $quba->get_slots());
|
||||
|
||||
$qa = $quba->get_question_attempt(1);
|
||||
$this->assertEquals(0, $qa->get_num_steps());
|
||||
}
|
||||
|
||||
public function test_load_data_no_qas() {
|
||||
// The code had a bug where if a question_usage had no question_attempts,
|
||||
// load_from_records got stuck in an infinite loop. This test is to
|
||||
// verify that no longer happens.
|
||||
$scid = context_system::instance()->id;
|
||||
$records = new question_test_recordset(array(
|
||||
array('qubaid', 'contextid', 'component', 'preferredbehaviour',
|
||||
'questionattemptid', 'questionusageid', 'slot',
|
||||
'behaviour', 'questionid', 'variant', 'maxmark', 'minfraction', 'maxfraction', 'flagged',
|
||||
'questionsummary', 'rightanswer', 'responsesummary', 'timemodified',
|
||||
'attemptstepid', 'sequencenumber', 'state', 'fraction',
|
||||
'timecreated', 'userid', 'name', 'value'),
|
||||
array(1, $scid, 'unit_test', 'interactive', null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null),
|
||||
));
|
||||
|
||||
question_bank::start_unit_test();
|
||||
$quba = question_usage_by_activity::load_from_records($records, 1);
|
||||
question_bank::end_unit_test();
|
||||
|
||||
$this->assertEquals('unit_test', $quba->get_owning_component());
|
||||
$this->assertEquals(1, $quba->get_id());
|
||||
$this->assertInstanceOf('question_engine_unit_of_work', $quba->get_observer());
|
||||
$this->assertEquals('interactive', $quba->get_preferred_behaviour());
|
||||
|
||||
$this->assertEquals(array(), $quba->get_slots());
|
||||
}
|
||||
}
|
@ -17,10 +17,9 @@
|
||||
/**
|
||||
* This file contains tests for the question_usage_by_activity class.
|
||||
*
|
||||
* @package moodlecore
|
||||
* @subpackage questionengine
|
||||
* @copyright 2009 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @package core_question
|
||||
* @copyright 2009 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
|
||||
@ -160,125 +159,3 @@ class question_usage_by_activity_test extends advanced_testcase {
|
||||
$quba->process_all_actions($slot, $postdata);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unit tests for loading data into the {@link question_usage_by_activity} class.
|
||||
*
|
||||
* @copyright 2012 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class question_usage_db_test extends data_loading_method_test_base {
|
||||
public function test_load() {
|
||||
$scid = context_system::instance()->id;
|
||||
$records = new question_test_recordset(array(
|
||||
array('qubaid', 'contextid', 'component', 'preferredbehaviour',
|
||||
'questionattemptid', 'questionusageid', 'slot',
|
||||
'behaviour', 'questionid', 'variant', 'maxmark', 'minfraction', 'maxfraction', 'flagged',
|
||||
'questionsummary', 'rightanswer', 'responsesummary', 'timemodified',
|
||||
'attemptstepid', 'sequencenumber', 'state', 'fraction',
|
||||
'timecreated', 'userid', 'name', 'value'),
|
||||
array(1, $scid, 'unit_test', 'interactive', 1, 1, 1, 'interactive', -1, 1, 2.0000000, 0.0000000, 1.0000000, 0, '', '', '', 1256233790, 1, 0, 'todo', null, 1256233700, 1, null, null),
|
||||
array(1, $scid, 'unit_test', 'interactive', 1, 1, 1, 'interactive', -1, 1, 2.0000000, 0.0000000, 1.0000000, 0, '', '', '', 1256233790, 2, 1, 'todo', null, 1256233705, 1, 'answer', '1'),
|
||||
array(1, $scid, 'unit_test', 'interactive', 1, 1, 1, 'interactive', -1, 1, 2.0000000, 0.0000000, 1.0000000, 0, '', '', '', 1256233790, 5, 2, 'gradedright', 1.0000000, 1256233720, 1, '-finish', '1'),
|
||||
));
|
||||
|
||||
$question = test_question_maker::make_question('truefalse', 'true');
|
||||
$question->id = -1;
|
||||
|
||||
question_bank::start_unit_test();
|
||||
question_bank::load_test_question_data($question);
|
||||
$quba = question_usage_by_activity::load_from_records($records, 1);
|
||||
question_bank::end_unit_test();
|
||||
|
||||
$this->assertEquals('unit_test', $quba->get_owning_component());
|
||||
$this->assertEquals(1, $quba->get_id());
|
||||
$this->assertInstanceOf('question_engine_unit_of_work', $quba->get_observer());
|
||||
$this->assertEquals('interactive', $quba->get_preferred_behaviour());
|
||||
|
||||
$qa = $quba->get_question_attempt(1);
|
||||
|
||||
$this->assertEquals($question->questiontext, $qa->get_question()->questiontext);
|
||||
|
||||
$this->assertEquals(3, $qa->get_num_steps());
|
||||
|
||||
$step = $qa->get_step(0);
|
||||
$this->assertEquals(question_state::$todo, $step->get_state());
|
||||
$this->assertNull($step->get_fraction());
|
||||
$this->assertEquals(1256233700, $step->get_timecreated());
|
||||
$this->assertEquals(1, $step->get_user_id());
|
||||
$this->assertEquals(array(), $step->get_all_data());
|
||||
|
||||
$step = $qa->get_step(1);
|
||||
$this->assertEquals(question_state::$todo, $step->get_state());
|
||||
$this->assertNull($step->get_fraction());
|
||||
$this->assertEquals(1256233705, $step->get_timecreated());
|
||||
$this->assertEquals(1, $step->get_user_id());
|
||||
$this->assertEquals(array('answer' => '1'), $step->get_all_data());
|
||||
|
||||
$step = $qa->get_step(2);
|
||||
$this->assertEquals(question_state::$gradedright, $step->get_state());
|
||||
$this->assertEquals(1, $step->get_fraction());
|
||||
$this->assertEquals(1256233720, $step->get_timecreated());
|
||||
$this->assertEquals(1, $step->get_user_id());
|
||||
$this->assertEquals(array('-finish' => '1'), $step->get_all_data());
|
||||
}
|
||||
|
||||
public function test_load_data_no_steps() {
|
||||
// The code had a bug where if one question_attempt had no steps,
|
||||
// load_from_records got stuck in an infinite loop. This test is to
|
||||
// verify that no longer happens.
|
||||
$scid = context_system::instance()->id;
|
||||
$records = new question_test_recordset(array(
|
||||
array('qubaid', 'contextid', 'component', 'preferredbehaviour',
|
||||
'questionattemptid', 'questionusageid', 'slot',
|
||||
'behaviour', 'questionid', 'variant', 'maxmark', 'minfraction', 'maxfraction', 'flagged',
|
||||
'questionsummary', 'rightanswer', 'responsesummary', 'timemodified',
|
||||
'attemptstepid', 'sequencenumber', 'state', 'fraction',
|
||||
'timecreated', 'userid', 'name', 'value'),
|
||||
array(1, $scid, 'unit_test', 'interactive', 1, 1, 1, 'interactive', 0, 1, 1.0000000, 0.0000000, 1.0000000, 0, 'This question is missing. Unable to display anything.', '', '', 0, null, null, null, null, null, null, null, null),
|
||||
array(1, $scid, 'unit_test', 'interactive', 2, 1, 2, 'interactive', 0, 1, 1.0000000, 0.0000000, 1.0000000, 0, 'This question is missing. Unable to display anything.', '', '', 0, null, null, null, null, null, null, null, null),
|
||||
array(1, $scid, 'unit_test', 'interactive', 3, 1, 3, 'interactive', 0, 1, 1.0000000, 0.0000000, 1.0000000, 0, 'This question is missing. Unable to display anything.', '', '', 0, null, null, null, null, null, null, null, null),
|
||||
));
|
||||
|
||||
question_bank::start_unit_test();
|
||||
$quba = question_usage_by_activity::load_from_records($records, 1);
|
||||
question_bank::end_unit_test();
|
||||
|
||||
$this->assertEquals('unit_test', $quba->get_owning_component());
|
||||
$this->assertEquals(1, $quba->get_id());
|
||||
$this->assertInstanceOf('question_engine_unit_of_work', $quba->get_observer());
|
||||
$this->assertEquals('interactive', $quba->get_preferred_behaviour());
|
||||
|
||||
$this->assertEquals(array(1, 2, 3), $quba->get_slots());
|
||||
|
||||
$qa = $quba->get_question_attempt(1);
|
||||
$this->assertEquals(0, $qa->get_num_steps());
|
||||
}
|
||||
|
||||
public function test_load_data_no_qas() {
|
||||
// The code had a bug where if a question_usage had no question_attempts,
|
||||
// load_from_records got stuck in an infinite loop. This test is to
|
||||
// verify that no longer happens.
|
||||
$scid = context_system::instance()->id;
|
||||
$records = new question_test_recordset(array(
|
||||
array('qubaid', 'contextid', 'component', 'preferredbehaviour',
|
||||
'questionattemptid', 'questionusageid', 'slot',
|
||||
'behaviour', 'questionid', 'variant', 'maxmark', 'minfraction', 'maxfraction', 'flagged',
|
||||
'questionsummary', 'rightanswer', 'responsesummary', 'timemodified',
|
||||
'attemptstepid', 'sequencenumber', 'state', 'fraction',
|
||||
'timecreated', 'userid', 'name', 'value'),
|
||||
array(1, $scid, 'unit_test', 'interactive', null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null),
|
||||
));
|
||||
|
||||
question_bank::start_unit_test();
|
||||
$quba = question_usage_by_activity::load_from_records($records, 1);
|
||||
question_bank::end_unit_test();
|
||||
|
||||
$this->assertEquals('unit_test', $quba->get_owning_component());
|
||||
$this->assertEquals(1, $quba->get_id());
|
||||
$this->assertInstanceOf('question_engine_unit_of_work', $quba->get_observer());
|
||||
$this->assertEquals('interactive', $quba->get_preferred_behaviour());
|
||||
|
||||
$this->assertEquals(array(), $quba->get_slots());
|
||||
}
|
||||
}
|
||||
|
124
question/tests/least_used_variant_strategy_test.php
Normal file
124
question/tests/least_used_variant_strategy_test.php
Normal file
@ -0,0 +1,124 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Tests for the {@link core_question\engine\variants\least_used_strategy} class.
|
||||
*
|
||||
* @package core_question
|
||||
* @copyright 2015 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
global $CFG;
|
||||
require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
|
||||
|
||||
|
||||
/**
|
||||
* Tests for the {@link core_question\engine\variants\least_used_strategy} class.
|
||||
*
|
||||
* @copyright 2015 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class least_used_variant_strategy_testcase extends advanced_testcase {
|
||||
|
||||
public function test_question_with_one_variant_always_picks_that() {
|
||||
$question = test_question_maker::make_question('shortanswer');
|
||||
$quba = question_engine::make_questions_usage_by_activity('test', context_system::instance());
|
||||
$quba->set_preferred_behaviour('deferredfeedback');
|
||||
$slot = $quba->add_question($question);
|
||||
$quba->start_all_questions(new core_question\engine\variants\least_used_strategy(
|
||||
$quba, new qubaid_list(array())));
|
||||
$this->assertEquals(1, $quba->get_variant($slot));
|
||||
}
|
||||
|
||||
public function test_synchronised_question_should_use_the_same_dataset() {
|
||||
// Actually, we cheat here. We use the same question twice, not two different synchronised questions.
|
||||
$question = test_question_maker::make_question('calculated');
|
||||
$quba = question_engine::make_questions_usage_by_activity('test', context_system::instance());
|
||||
$quba->set_preferred_behaviour('deferredfeedback');
|
||||
$slot1 = $quba->add_question($question);
|
||||
$slot2 = $quba->add_question($question);
|
||||
$quba->start_all_questions(new core_question\engine\variants\least_used_strategy(
|
||||
$quba, new qubaid_list(array())));
|
||||
$this->assertEquals($quba->get_variant($slot1), $quba->get_variant($slot2));
|
||||
}
|
||||
|
||||
public function test_second_attempt_uses_other_dataset() {
|
||||
global $DB;
|
||||
$this->resetAfterTest();
|
||||
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
|
||||
$cat = $generator->create_question_category();
|
||||
$questiondata = $generator->create_question('calculated', null, array('category' => $cat->id));
|
||||
|
||||
// Create two dataset items.
|
||||
$adefinitionid = $DB->get_field_sql("
|
||||
SELECT qdd.id
|
||||
FROM {question_dataset_definitions} qdd
|
||||
JOIN {question_datasets} qd ON qd.datasetdefinition = qdd.id
|
||||
WHERE qd.question = ?
|
||||
AND qdd.name = ?", array($questiondata->id, 'a'));
|
||||
$bdefinitionid = $DB->get_field_sql("
|
||||
SELECT qdd.id
|
||||
FROM {question_dataset_definitions} qdd
|
||||
JOIN {question_datasets} qd ON qd.datasetdefinition = qdd.id
|
||||
WHERE qd.question = ?
|
||||
AND qdd.name = ?", array($questiondata->id, 'b'));
|
||||
$DB->set_field('question_dataset_definitions', 'itemcount', 2, array('id' => $adefinitionid));
|
||||
$DB->set_field('question_dataset_definitions', 'itemcount', 2, array('id' => $bdefinitionid));
|
||||
$DB->insert_record('question_dataset_items', array('definition' => $adefinitionid,
|
||||
'itemnumber' => 1, 'value' => 3));
|
||||
$DB->insert_record('question_dataset_items', array('definition' => $bdefinitionid,
|
||||
'itemnumber' => 1, 'value' => 7));
|
||||
$DB->insert_record('question_dataset_items', array('definition' => $adefinitionid,
|
||||
'itemnumber' => 2, 'value' => 6));
|
||||
$DB->insert_record('question_dataset_items', array('definition' => $bdefinitionid,
|
||||
'itemnumber' => 2, 'value' => 4));
|
||||
|
||||
$question = question_bank::load_question($questiondata->id);
|
||||
|
||||
$quba1 = question_engine::make_questions_usage_by_activity('test', context_system::instance());
|
||||
$quba1->set_preferred_behaviour('deferredfeedback');
|
||||
$slot1 = $quba1->add_question($question);
|
||||
$quba1->start_all_questions(new core_question\engine\variants\least_used_strategy(
|
||||
$quba1, new qubaid_list(array())));
|
||||
question_engine::save_questions_usage_by_activity($quba1);
|
||||
$variant1 = $quba1->get_variant($slot1);
|
||||
|
||||
// Second attempt should use the other variant.
|
||||
$quba2 = question_engine::make_questions_usage_by_activity('test', context_system::instance());
|
||||
$quba2->set_preferred_behaviour('deferredfeedback');
|
||||
$slot2 = $quba2->add_question($question);
|
||||
$quba2->start_all_questions(new core_question\engine\variants\least_used_strategy(
|
||||
$quba1, new qubaid_list(array($quba1->get_id()))));
|
||||
question_engine::save_questions_usage_by_activity($quba2);
|
||||
$variant2 = $quba2->get_variant($slot2);
|
||||
|
||||
$this->assertNotEquals($variant1, $variant2);
|
||||
|
||||
// Third attempt uses either variant at random.
|
||||
$quba3 = question_engine::make_questions_usage_by_activity('test', context_system::instance());
|
||||
$quba3->set_preferred_behaviour('deferredfeedback');
|
||||
$slot3 = $quba3->add_question($question);
|
||||
$quba3->start_all_questions(new core_question\engine\variants\least_used_strategy(
|
||||
$quba1, new qubaid_list(array($quba1->get_id(), $quba2->get_id()))));
|
||||
$variant3 = $quba3->get_variant($slot3);
|
||||
|
||||
$this->assertTrue($variant3 == $variant1 || $variant3 == $variant2);
|
||||
}
|
||||
}
|
182
question/tests/random_question_loader_test.php
Normal file
182
question/tests/random_question_loader_test.php
Normal file
@ -0,0 +1,182 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Tests for the {@link core_question\bank\random_question_loader} class.
|
||||
*
|
||||
* @package core_question
|
||||
* @copyright 2015 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
|
||||
/**
|
||||
* Tests for the {@link core_question\bank\random_question_loader} class.
|
||||
*
|
||||
* @copyright 2015 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class random_question_loader_testcase extends advanced_testcase {
|
||||
|
||||
public function test_empty_category_gives_null() {
|
||||
$this->resetAfterTest();
|
||||
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
|
||||
$cat = $generator->create_question_category();
|
||||
$loader = new \core_question\bank\random_question_loader(new qubaid_list(array()));
|
||||
|
||||
$this->assertNull($loader->get_next_question_id($cat->id, 0));
|
||||
$this->assertNull($loader->get_next_question_id($cat->id, 1));
|
||||
}
|
||||
|
||||
public function test_unknown_category_behaves_like_empty() {
|
||||
// It is up the caller to make sure the category id is valid.
|
||||
$loader = new \core_question\bank\random_question_loader(new qubaid_list(array()));
|
||||
$this->assertNull($loader->get_next_question_id(-1, 1));
|
||||
}
|
||||
|
||||
public function test_descriptions_not_returned() {
|
||||
$this->resetAfterTest();
|
||||
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
|
||||
$cat = $generator->create_question_category();
|
||||
$info = $generator->create_question('description', null, array('category' => $cat->id));
|
||||
$loader = new \core_question\bank\random_question_loader(new qubaid_list(array()));
|
||||
|
||||
$this->assertNull($loader->get_next_question_id($cat->id, 0));
|
||||
}
|
||||
|
||||
public function test_one_question_category_returns_that_q_then_null() {
|
||||
$this->resetAfterTest();
|
||||
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
|
||||
$cat = $generator->create_question_category();
|
||||
$question1 = $generator->create_question('shortanswer', null, array('category' => $cat->id));
|
||||
$loader = new \core_question\bank\random_question_loader(new qubaid_list(array()));
|
||||
|
||||
$this->assertEquals($question1->id, $loader->get_next_question_id($cat->id, 1));
|
||||
$this->assertNull($loader->get_next_question_id($cat->id, 0));
|
||||
}
|
||||
|
||||
public function test_two_question_category_returns_both_then_null() {
|
||||
$this->resetAfterTest();
|
||||
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
|
||||
$cat = $generator->create_question_category();
|
||||
$question1 = $generator->create_question('shortanswer', null, array('category' => $cat->id));
|
||||
$question2 = $generator->create_question('shortanswer', null, array('category' => $cat->id));
|
||||
$loader = new \core_question\bank\random_question_loader(new qubaid_list(array()));
|
||||
|
||||
$questionids = array();
|
||||
$questionids[] = $loader->get_next_question_id($cat->id, 0);
|
||||
$questionids[] = $loader->get_next_question_id($cat->id, 0);
|
||||
sort($questionids);
|
||||
$this->assertEquals(array($question1->id, $question2->id), $questionids);
|
||||
|
||||
$this->assertNull($loader->get_next_question_id($cat->id, 1));
|
||||
}
|
||||
|
||||
public function test_nested_categories() {
|
||||
$this->resetAfterTest();
|
||||
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
|
||||
$cat1 = $generator->create_question_category();
|
||||
$cat2 = $generator->create_question_category(array('parent' => $cat1->id));
|
||||
$question1 = $generator->create_question('shortanswer', null, array('category' => $cat1->id));
|
||||
$question2 = $generator->create_question('shortanswer', null, array('category' => $cat2->id));
|
||||
$loader = new \core_question\bank\random_question_loader(new qubaid_list(array()));
|
||||
|
||||
$this->assertEquals($question2->id, $loader->get_next_question_id($cat2->id, 1));
|
||||
$this->assertEquals($question1->id, $loader->get_next_question_id($cat1->id, 1));
|
||||
|
||||
$this->assertNull($loader->get_next_question_id($cat1->id, 0));
|
||||
}
|
||||
|
||||
public function test_used_question_not_returned_until_later() {
|
||||
$this->resetAfterTest();
|
||||
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
|
||||
$cat = $generator->create_question_category();
|
||||
$question1 = $generator->create_question('shortanswer', null, array('category' => $cat->id));
|
||||
$question2 = $generator->create_question('shortanswer', null, array('category' => $cat->id));
|
||||
$loader = new \core_question\bank\random_question_loader(new qubaid_list(array()),
|
||||
array($question2->id => 2));
|
||||
|
||||
$this->assertEquals($question1->id, $loader->get_next_question_id($cat->id, 0));
|
||||
$this->assertNull($loader->get_next_question_id($cat->id, 0));
|
||||
}
|
||||
|
||||
public function test_previously_used_question_not_returned_until_later() {
|
||||
$this->resetAfterTest();
|
||||
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
|
||||
$cat = $generator->create_question_category();
|
||||
$question1 = $generator->create_question('shortanswer', null, array('category' => $cat->id));
|
||||
$question2 = $generator->create_question('shortanswer', null, array('category' => $cat->id));
|
||||
$quba = question_engine::make_questions_usage_by_activity('test', context_system::instance());
|
||||
$quba->set_preferred_behaviour('deferredfeedback');
|
||||
$question = question_bank::load_question($question2->id);
|
||||
$quba->add_question($question);
|
||||
$quba->add_question($question);
|
||||
$quba->start_all_questions();
|
||||
question_engine::save_questions_usage_by_activity($quba);
|
||||
|
||||
$loader = new \core_question\bank\random_question_loader(new qubaid_list(array($quba->get_id())));
|
||||
|
||||
$this->assertEquals($question1->id, $loader->get_next_question_id($cat->id, 0));
|
||||
$this->assertEquals($question2->id, $loader->get_next_question_id($cat->id, 0));
|
||||
$this->assertNull($loader->get_next_question_id($cat->id, 0));
|
||||
}
|
||||
|
||||
public function test_empty_category_does_not_have_question_available() {
|
||||
$this->resetAfterTest();
|
||||
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
|
||||
$cat = $generator->create_question_category();
|
||||
$loader = new \core_question\bank\random_question_loader(new qubaid_list(array()));
|
||||
|
||||
$this->assertFalse($loader->is_question_available($cat->id, 0, 1));
|
||||
$this->assertFalse($loader->is_question_available($cat->id, 1, 1));
|
||||
}
|
||||
|
||||
public function test_descriptions_not_available() {
|
||||
$this->resetAfterTest();
|
||||
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
|
||||
$cat = $generator->create_question_category();
|
||||
$info = $generator->create_question('description', null, array('category' => $cat->id));
|
||||
$loader = new \core_question\bank\random_question_loader(new qubaid_list(array()));
|
||||
|
||||
$this->assertFalse($loader->is_question_available($cat->id, 0, $info->id));
|
||||
$this->assertFalse($loader->is_question_available($cat->id, 1, $info->id));
|
||||
}
|
||||
|
||||
public function test_existing_question_is_available_but_then_marked_used() {
|
||||
$this->resetAfterTest();
|
||||
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
|
||||
$cat = $generator->create_question_category();
|
||||
$question1 = $generator->create_question('shortanswer', null, array('category' => $cat->id));
|
||||
$loader = new \core_question\bank\random_question_loader(new qubaid_list(array()));
|
||||
|
||||
$this->assertTrue($loader->is_question_available($cat->id, 0, $question1->id));
|
||||
$this->assertFalse($loader->is_question_available($cat->id, 0, $question1->id));
|
||||
|
||||
$this->assertFalse($loader->is_question_available($cat->id, 0, -1));
|
||||
}
|
||||
}
|
@ -120,6 +120,74 @@ class qtype_calculated_test_helper extends question_test_helper {
|
||||
|
||||
return $qdata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a calculated question about summing two numbers.
|
||||
* @return qtype_calculated_question
|
||||
*/
|
||||
public function get_calculated_question_form_data_sum() {
|
||||
question_bank::load_question_definition_classes('calculated');
|
||||
$fromform = new stdClass();
|
||||
|
||||
$fromform->name = 'Simple sum';
|
||||
$fromform->questiontext = 'What is {a} + {b}?';
|
||||
$fromform->defaultmark = 1.0;
|
||||
$fromform->generalfeedback = 'Generalfeedback: {={a} + {b}} is the right answer.';
|
||||
|
||||
$fromform->unitrole = '3';
|
||||
$fromform->unitpenalty = 0.1;
|
||||
$fromform->unitgradingtypes = '1';
|
||||
$fromform->unitsleft = '0';
|
||||
$fromform->nounits = 1;
|
||||
$fromform->multiplier = array();
|
||||
$fromform->multiplier[0] = '1.0';
|
||||
$fromform->synchronize = 0;
|
||||
$fromform->answernumbering = 0;
|
||||
$fromform->shuffleanswers = 0;
|
||||
|
||||
$fromform->noanswers = 6;
|
||||
$fromform->answer = array();
|
||||
$fromform->answer[0] = '{a} + {b}';
|
||||
$fromform->answer[1] = '{a} - {b}';
|
||||
$fromform->answer[2] = '*';
|
||||
|
||||
$fromform->fraction = array();
|
||||
$fromform->fraction[0] = '1.0';
|
||||
$fromform->fraction[1] = '0.0';
|
||||
$fromform->fraction[2] = '0.0';
|
||||
|
||||
$fromform->tolerance = array();
|
||||
$fromform->tolerance[0] = 0.001;
|
||||
$fromform->tolerance[1] = 0.001;
|
||||
$fromform->tolerance[2] = 0;
|
||||
|
||||
$fromform->tolerancetype[0] = 1;
|
||||
$fromform->tolerancetype[1] = 1;
|
||||
$fromform->tolerancetype[2] = 1;
|
||||
|
||||
$fromform->correctanswerlength[0] = 2;
|
||||
$fromform->correctanswerlength[1] = 2;
|
||||
$fromform->correctanswerlength[2] = 2;
|
||||
|
||||
$fromform->correctanswerformat[0] = 1;
|
||||
$fromform->correctanswerformat[1] = 1;
|
||||
$fromform->correctanswerformat[2] = 1;
|
||||
|
||||
$fromform->feedback = array();
|
||||
$fromform->feedback[0] = array();
|
||||
$fromform->feedback[0]['format'] = FORMAT_HTML;
|
||||
$fromform->feedback[0]['text'] = 'Very good.';
|
||||
|
||||
$fromform->feedback[1] = array();
|
||||
$fromform->feedback[1]['format'] = FORMAT_HTML;
|
||||
$fromform->feedback[1]['text'] = 'Add. not subtract!';
|
||||
|
||||
$fromform->feedback[2] = array();
|
||||
$fromform->feedback[2]['format'] = FORMAT_HTML;
|
||||
$fromform->feedback[2]['text'] = 'Completely wrong.';
|
||||
|
||||
return $fromform;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user