PageRenderTime 22ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/question/type/random/questiontype.php

http://github.com/moodle/moodle
PHP | 314 lines | 183 code | 40 blank | 91 comment | 29 complexity | c6e0d248b21d709e52cc88eaa3139cb7 MD5 | raw file
Possible License(s): MIT, AGPL-3.0, MPL-2.0-no-copyleft-exception, LGPL-3.0, GPL-3.0, Apache-2.0, LGPL-2.1, BSD-3-Clause
  1. <?php
  2. // This file is part of Moodle - http://moodle.org/
  3. //
  4. // Moodle is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // Moodle is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU General Public License
  15. // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
  16. /**
  17. * Question type class for the random question type.
  18. *
  19. * @package qtype
  20. * @subpackage random
  21. * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
  22. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23. */
  24. defined('MOODLE_INTERNAL') || die();
  25. require_once($CFG->dirroot . '/question/type/questiontypebase.php');
  26. /**
  27. * The random question type.
  28. *
  29. * This question type does not have a question definition class, nor any
  30. * renderers. When you load a question of this type, it actually loads a
  31. * question chosen randomly from a particular category in the question bank.
  32. *
  33. * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
  34. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  35. */
  36. class qtype_random extends question_type {
  37. /** @var string comma-separated list of qytpe names not to select, can be used in SQL. */
  38. protected $excludedqtypes = null;
  39. /** @var string comma-separated list of manually graded qytpe names, can be used in SQL. */
  40. protected $manualqtypes = null;
  41. /**
  42. * Cache of availabe question ids from a particular category.
  43. * @var array two-dimensional array. The first key is a category id, the
  44. * second key is wether subcategories should be included.
  45. */
  46. private $availablequestionsbycategory = array();
  47. public function menu_name() {
  48. // Don't include this question type in the 'add new question' menu.
  49. return false;
  50. }
  51. public function is_manual_graded() {
  52. return true;
  53. }
  54. public function is_usable_by_random() {
  55. return false;
  56. }
  57. public function is_question_manual_graded($question, $otherquestionsinuse) {
  58. global $DB;
  59. // We take our best shot at working whether a particular question is manually
  60. // graded follows: We look to see if any of the questions that this random
  61. // question might select if of a manually graded type. If a category contains
  62. // a mixture of manual and non-manual questions, and if all the attempts so
  63. // far selected non-manual ones, this will give the wrong answer, but we
  64. // don't care. Even so, this is an expensive calculation!
  65. $this->init_qtype_lists();
  66. if (!$this->manualqtypes) {
  67. return false;
  68. }
  69. if ($question->questiontext) {
  70. $categorylist = question_categorylist($question->category);
  71. } else {
  72. $categorylist = array($question->category);
  73. }
  74. list($qcsql, $qcparams) = $DB->get_in_or_equal($categorylist);
  75. // TODO use in_or_equal for $otherquestionsinuse and $this->manualqtypes.
  76. return $DB->record_exists_select('question',
  77. "category {$qcsql}
  78. AND parent = 0
  79. AND hidden = 0
  80. AND id NOT IN ($otherquestionsinuse)
  81. AND qtype IN ($this->manualqtypes)", $qcparams);
  82. }
  83. /**
  84. * This method needs to be called before the ->excludedqtypes and
  85. * ->manualqtypes fields can be used.
  86. */
  87. protected function init_qtype_lists() {
  88. if (!is_null($this->excludedqtypes)) {
  89. return; // Already done.
  90. }
  91. $excludedqtypes = array();
  92. $manualqtypes = array();
  93. foreach (question_bank::get_all_qtypes() as $qtype) {
  94. $quotedname = "'" . $qtype->name() . "'";
  95. if (!$qtype->is_usable_by_random()) {
  96. $excludedqtypes[] = $quotedname;
  97. } else if ($qtype->is_manual_graded()) {
  98. $manualqtypes[] = $quotedname;
  99. }
  100. }
  101. $this->excludedqtypes = implode(',', $excludedqtypes);
  102. $this->manualqtypes = implode(',', $manualqtypes);
  103. }
  104. public function get_question_options($question) {
  105. parent::get_question_options($question);
  106. return true;
  107. }
  108. /**
  109. * Random questions always get a question name that is Random (cateogryname).
  110. * This function is a centralised place to calculate that, given the category.
  111. * @param stdClass $category the category this question picks from. (Only ->name is used.)
  112. * @param bool $includesubcategories whether this question also picks from subcategories.
  113. * @param string[] $tagnames Name of tags this question picks from.
  114. * @return string the name this question should have.
  115. */
  116. public function question_name($category, $includesubcategories, $tagnames = []) {
  117. $categoryname = '';
  118. if ($category->parent && $includesubcategories) {
  119. $stringid = 'randomqplusname';
  120. $categoryname = shorten_text($category->name, 100);
  121. } else if ($category->parent) {
  122. $stringid = 'randomqname';
  123. $categoryname = shorten_text($category->name, 100);
  124. } else if ($includesubcategories) {
  125. $context = context::instance_by_id($category->contextid);
  126. switch ($context->contextlevel) {
  127. case CONTEXT_MODULE:
  128. $stringid = 'randomqplusnamemodule';
  129. break;
  130. case CONTEXT_COURSE:
  131. $stringid = 'randomqplusnamecourse';
  132. break;
  133. case CONTEXT_COURSECAT:
  134. $stringid = 'randomqplusnamecoursecat';
  135. $categoryname = shorten_text($context->get_context_name(false), 100);
  136. break;
  137. case CONTEXT_SYSTEM:
  138. $stringid = 'randomqplusnamesystem';
  139. break;
  140. default: // Impossible.
  141. }
  142. } else {
  143. // No question will ever be selected. So, let's warn the teacher.
  144. $stringid = 'randomqnamefromtop';
  145. }
  146. if ($tagnames) {
  147. $stringid .= 'tags';
  148. $a = new stdClass();
  149. if ($categoryname) {
  150. $a->category = $categoryname;
  151. }
  152. $a->tags = implode(',', array_map(function($tagname) {
  153. return explode(',', $tagname)[1];
  154. }, $tagnames));
  155. } else {
  156. $a = $categoryname ? : null;
  157. }
  158. $name = get_string($stringid, 'qtype_random', $a);
  159. return shorten_text($name, 255);
  160. }
  161. protected function set_selected_question_name($question, $randomname) {
  162. $a = new stdClass();
  163. $a->randomname = $randomname;
  164. $a->questionname = $question->name;
  165. $question->name = get_string('selectedby', 'qtype_random', $a);
  166. }
  167. public function save_question($question, $form) {
  168. global $DB;
  169. $form->name = '';
  170. list($category) = explode(',', $form->category);
  171. if (!$form->includesubcategories) {
  172. if ($DB->record_exists('question_categories', ['id' => $category, 'parent' => 0])) {
  173. // The chosen category is a top category.
  174. $form->includesubcategories = true;
  175. }
  176. }
  177. $form->tags = array();
  178. if (empty($form->fromtags)) {
  179. $form->fromtags = array();
  180. }
  181. $form->questiontext = array(
  182. 'text' => $form->includesubcategories ? '1' : '0',
  183. 'format' => 0
  184. );
  185. // Name is not a required field for random questions, but
  186. // parent::save_question Assumes that it is.
  187. return parent::save_question($question, $form);
  188. }
  189. public function save_question_options($question) {
  190. global $DB;
  191. // No options, as such, but we set the parent field to the question's
  192. // own id. Setting the parent field has the effect of hiding this
  193. // question in various places.
  194. $updateobject = new stdClass();
  195. $updateobject->id = $question->id;
  196. $updateobject->parent = $question->id;
  197. // We also force the question name to be 'Random (categoryname)'.
  198. $category = $DB->get_record('question_categories',
  199. array('id' => $question->category), '*', MUST_EXIST);
  200. $updateobject->name = $this->question_name($category, $question->includesubcategories, $question->fromtags);
  201. return $DB->update_record('question', $updateobject);
  202. }
  203. /**
  204. * During unit tests we need to be able to reset all caches so that each new test starts in a known state.
  205. * Intended for use only for testing. This is a stop gap until we start using the MUC caching api here.
  206. * You need to call this before every test that loads one or more random questions.
  207. */
  208. public function clear_caches_before_testing() {
  209. $this->availablequestionsbycategory = array();
  210. }
  211. /**
  212. * Get all the usable questions from a particular question category.
  213. *
  214. * @param int $categoryid the id of a question category.
  215. * @param bool whether to include questions from subcategories.
  216. * @param string $questionsinuse comma-separated list of question ids to
  217. * exclude from consideration.
  218. * @return array of question records.
  219. */
  220. public function get_available_questions_from_category($categoryid, $subcategories) {
  221. if (isset($this->availablequestionsbycategory[$categoryid][$subcategories])) {
  222. return $this->availablequestionsbycategory[$categoryid][$subcategories];
  223. }
  224. $this->init_qtype_lists();
  225. if ($subcategories) {
  226. $categoryids = question_categorylist($categoryid);
  227. } else {
  228. $categoryids = array($categoryid);
  229. }
  230. $questionids = question_bank::get_finder()->get_questions_from_categories(
  231. $categoryids, 'qtype NOT IN (' . $this->excludedqtypes . ')');
  232. $this->availablequestionsbycategory[$categoryid][$subcategories] = $questionids;
  233. return $questionids;
  234. }
  235. public function make_question($questiondata) {
  236. return $this->choose_other_question($questiondata, array());
  237. }
  238. /**
  239. * Load the definition of another question picked randomly by this question.
  240. * @param object $questiondata the data defining a random question.
  241. * @param array $excludedquestions of question ids. We will no pick any question whose id is in this list.
  242. * @param bool $allowshuffle if false, then any shuffle option on the selected quetsion is disabled.
  243. * @param null|integer $forcequestionid if not null then force the picking of question with id $forcequestionid.
  244. * @throws coding_exception
  245. * @return question_definition|null the definition of the question that was
  246. * selected, or null if no suitable question could be found.
  247. */
  248. public function choose_other_question($questiondata, $excludedquestions, $allowshuffle = true, $forcequestionid = null) {
  249. $available = $this->get_available_questions_from_category($questiondata->category,
  250. !empty($questiondata->questiontext));
  251. shuffle($available);
  252. if ($forcequestionid !== null) {
  253. $forcedquestionkey = array_search($forcequestionid, $available);
  254. if ($forcedquestionkey !== false) {
  255. unset($available[$forcedquestionkey]);
  256. array_unshift($available, $forcequestionid);
  257. } else {
  258. throw new coding_exception('thisquestionidisnotavailable', $forcequestionid);
  259. }
  260. }
  261. foreach ($available as $questionid) {
  262. if (in_array($questionid, $excludedquestions)) {
  263. continue;
  264. }
  265. $question = question_bank::load_question($questionid, $allowshuffle);
  266. $this->set_selected_question_name($question, $questiondata->name);
  267. return $question;
  268. }
  269. return null;
  270. }
  271. public function get_random_guess_score($questiondata) {
  272. return null;
  273. }
  274. }