mirror of
https://github.com/moodle/moodle.git
synced 2025-04-22 08:55:15 +02:00
MDL-61380 Questions: Support selecting "random" by tag in question bank
This commit is contained in:
parent
82c67ac7f0
commit
2adefc21ac
@ -77,6 +77,7 @@ class random_question_loader {
|
||||
|
||||
/**
|
||||
* Pick a question at random from the given category, from among those with the fewest uses.
|
||||
* If an array of tag ids are specified, then only the questions that are tagged with ALL those tags will be selected.
|
||||
*
|
||||
* It is up the the caller to verify that the cateogry exists. An unknown category
|
||||
* behaves like an empty one.
|
||||
@ -84,12 +85,14 @@ class random_question_loader {
|
||||
* @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 array $tagids An array of tag ids. A question has to be tagged with all the provided tagids (if any)
|
||||
* in order to be eligible for being picked.
|
||||
* @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);
|
||||
public function get_next_question_id($categoryid, $includesubcategories, $tagids = []) {
|
||||
$this->ensure_questions_for_category_loaded($categoryid, $includesubcategories, $tagids);
|
||||
|
||||
$categorykey = $this->get_category_key($categoryid, $includesubcategories);
|
||||
$categorykey = $this->get_category_key($categoryid, $includesubcategories, $tagids);
|
||||
if (empty($this->availablequestionscache[$categorykey])) {
|
||||
return null;
|
||||
}
|
||||
@ -107,26 +110,35 @@ class random_question_loader {
|
||||
* @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 array $tagids an array of tag ids.
|
||||
* @return string the cache key.
|
||||
*/
|
||||
protected function get_category_key($categoryid, $includesubcategories) {
|
||||
protected function get_category_key($categoryid, $includesubcategories, $tagids = []) {
|
||||
if ($includesubcategories) {
|
||||
return $categoryid . '|1';
|
||||
$key = $categoryid . '|1';
|
||||
} else {
|
||||
return $categoryid . '|0';
|
||||
$key = $categoryid . '|0';
|
||||
}
|
||||
|
||||
if (!empty($tagids)) {
|
||||
$key .= '|' . implode('|', $tagids);
|
||||
}
|
||||
|
||||
return $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param int $categoryid The id of a category in the question bank.
|
||||
* @param bool $includesubcategories Whether to pick a question from exactly
|
||||
* that category, or that category and subcategories.
|
||||
* @param array $tagids An array of tag ids. If an array is provided, then
|
||||
* only the questions that are tagged with ALL the provided tagids will be loaded.
|
||||
*/
|
||||
protected function ensure_questions_for_category_loaded($categoryid, $includesubcategories) {
|
||||
protected function ensure_questions_for_category_loaded($categoryid, $includesubcategories, $tagids = []) {
|
||||
global $DB;
|
||||
|
||||
$categorykey = $this->get_category_key($categoryid, $includesubcategories);
|
||||
$categorykey = $this->get_category_key($categoryid, $includesubcategories, $tagids);
|
||||
|
||||
if (isset($this->availablequestionscache[$categorykey])) {
|
||||
// Data is already in the cache, nothing to do.
|
||||
@ -143,8 +155,8 @@ class random_question_loader {
|
||||
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);
|
||||
$questionidsandcounts = \question_bank::get_finder()->get_questions_from_categories_and_tags_with_usage_counts(
|
||||
$categoryids, $this->qubaids, 'q.qtype ' . $extraconditions, $extraparams, $tagids);
|
||||
if (!$questionidsandcounts) {
|
||||
// No questions in this category.
|
||||
$this->availablequestionscache[$categorykey] = array();
|
||||
@ -200,16 +212,19 @@ class random_question_loader {
|
||||
|
||||
/**
|
||||
* Check whether a given question is available in a given category. If so, mark it used.
|
||||
* If an optional list of tag ids are provided, then the question must be tagged with
|
||||
* ALL of the provided tags to be considered as available.
|
||||
*
|
||||
* @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.
|
||||
* @param array $tagids An array of tag ids. Only the questions that are tagged with all the provided tagids can be available.
|
||||
* @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);
|
||||
public function is_question_available($categoryid, $includesubcategories, $questionid, $tagids = []) {
|
||||
$this->ensure_questions_for_category_loaded($categoryid, $includesubcategories, $tagids);
|
||||
$categorykey = $this->get_category_key($categoryid, $includesubcategories, $tagids);
|
||||
|
||||
foreach ($this->availablequestionscache[$categorykey] as $questionids) {
|
||||
if (isset($questionids[$questionid])) {
|
||||
|
@ -294,10 +294,6 @@ abstract class question_bank {
|
||||
*/
|
||||
public static function get_finder() {
|
||||
return question_finder::get_instance();
|
||||
if (is_null(self::$questionfinder)) {
|
||||
self::$questionfinder = new question_finder();
|
||||
}
|
||||
return self::$questionfinder;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -539,29 +535,71 @@ class question_finder implements cache_data_source {
|
||||
*/
|
||||
public function get_questions_from_categories_with_usage_counts($categoryids,
|
||||
qubaid_condition $qubaids, $extraconditions = '', $extraparams = array()) {
|
||||
return $this->get_questions_from_categories_and_tags_with_usage_counts(
|
||||
$categoryids, $qubaids, $extraconditions, $extraparams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ids of all the questions in a list of categories that have ALL the provided tags,
|
||||
* 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 of 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.
|
||||
* @param array $tagids an array of tag ids
|
||||
* @return array questionid => count of number of previous uses.
|
||||
*/
|
||||
public function get_questions_from_categories_and_tags_with_usage_counts($categoryids,
|
||||
qubaid_condition $qubaids, $extraconditions = '', $extraparams = array(), $tagids = array()) {
|
||||
global $DB;
|
||||
|
||||
list($qcsql, $qcparams) = $DB->get_in_or_equal($categoryids, SQL_PARAMS_NAMED, 'qc');
|
||||
|
||||
$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}
|
||||
AND q.parent = 0
|
||||
AND q.hidden = 0";
|
||||
$params = $qcparams;
|
||||
|
||||
if (!empty($tagids)) {
|
||||
// We treat each additional tag as an AND condition rather than
|
||||
// an OR condition.
|
||||
//
|
||||
// For example, if the user filters by the tags "foo" and "bar" then
|
||||
// we reduce the question list to questions that are tagged with both
|
||||
// "foo" AND "bar". Any question that does not have ALL of the specified
|
||||
// tags will be omitted.
|
||||
list($tagsql, $tagparams) = $DB->get_in_or_equal($tagids, SQL_PARAMS_NAMED, 'ti');
|
||||
$tagparams['tagcount'] = count($tagids);
|
||||
$tagparams['questionitemtype'] = 'question';
|
||||
$tagparams['questioncomponent'] = 'core_question';
|
||||
$where .= " AND q.id IN (SELECT ti.itemid
|
||||
FROM {tag_instance} ti
|
||||
WHERE ti.itemtype = :questionitemtype
|
||||
AND ti.component = :questioncomponent
|
||||
AND ti.tagid {$tagsql}
|
||||
GROUP BY ti.itemid
|
||||
HAVING COUNT(itemid) = :tagcount)";
|
||||
$params += $tagparams;
|
||||
}
|
||||
|
||||
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}
|
||||
AND q.parent = 0
|
||||
AND q.hidden = 0
|
||||
{$extraconditions}
|
||||
|
||||
ORDER BY previous_attempts
|
||||
", $qubaids->from_where_params() + $qcparams + $extraparams);
|
||||
return $DB->get_records_sql_menu("SELECT $select
|
||||
FROM $from
|
||||
WHERE $where $extraconditions
|
||||
ORDER BY previous_attempts",
|
||||
$qubaids->from_where_params() + $params + $extraparams);
|
||||
}
|
||||
|
||||
/* See cache_data_source::load_for_cache. */
|
||||
|
Loading…
x
Reference in New Issue
Block a user