editlib.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  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. * Functions used to show question editing interface
  18. *
  19. * @package moodlecore
  20. * @subpackage questionbank
  21. * @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com}
  22. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23. */
  24. use core_question\bank\search\category_condition;
  25. defined('MOODLE_INTERNAL') || die();
  26. require_once($CFG->libdir . '/questionlib.php');
  27. define('DEFAULT_QUESTIONS_PER_PAGE', 20);
  28. define('MAXIMUM_QUESTIONS_PER_PAGE', 1000);
  29. function get_module_from_cmid($cmid) {
  30. global $CFG, $DB;
  31. if (!$cmrec = $DB->get_record_sql("SELECT cm.*, md.name as modname
  32. FROM {course_modules} cm,
  33. {modules} md
  34. WHERE cm.id = ? AND
  35. md.id = cm.module", array($cmid))){
  36. print_error('invalidcoursemodule');
  37. } elseif (!$modrec =$DB->get_record($cmrec->modname, array('id' => $cmrec->instance))) {
  38. print_error('invalidcoursemodule');
  39. }
  40. $modrec->instance = $modrec->id;
  41. $modrec->cmid = $cmrec->id;
  42. $cmrec->name = $modrec->name;
  43. return array($modrec, $cmrec);
  44. }
  45. /**
  46. * Function to read all questions for category into big array
  47. *
  48. * @param int $category category number
  49. * @param bool $noparent if true only questions with NO parent will be selected
  50. * @param bool $recurse include subdirectories
  51. * @param bool $export set true if this is called by questionbank export
  52. */
  53. function get_questions_category( $category, $noparent=false, $recurse=true, $export=true ) {
  54. global $DB;
  55. // Build sql bit for $noparent
  56. $npsql = '';
  57. if ($noparent) {
  58. $npsql = " and parent='0' ";
  59. }
  60. // Get list of categories
  61. if ($recurse) {
  62. $categorylist = question_categorylist($category->id);
  63. } else {
  64. $categorylist = array($category->id);
  65. }
  66. // Get the list of questions for the category
  67. list($usql, $params) = $DB->get_in_or_equal($categorylist);
  68. $questions = $DB->get_records_select('question', "category {$usql} {$npsql}", $params, 'qtype, name');
  69. // Iterate through questions, getting stuff we need
  70. $qresults = array();
  71. foreach($questions as $key => $question) {
  72. $question->export_process = $export;
  73. $qtype = question_bank::get_qtype($question->qtype, false);
  74. if ($export && $qtype->name() == 'missingtype') {
  75. // Unrecognised question type. Skip this question when exporting.
  76. continue;
  77. }
  78. $qtype->get_question_options($question);
  79. $qresults[] = $question;
  80. }
  81. return $qresults;
  82. }
  83. /**
  84. * @param int $categoryid a category id.
  85. * @return bool whether this is the only top-level category in a context.
  86. */
  87. function question_is_only_toplevel_category_in_context($categoryid) {
  88. global $DB;
  89. return 1 == $DB->count_records_sql("
  90. SELECT count(*)
  91. FROM {question_categories} c1,
  92. {question_categories} c2
  93. WHERE c2.id = ?
  94. AND c1.contextid = c2.contextid
  95. AND c1.parent = 0 AND c2.parent = 0", array($categoryid));
  96. }
  97. /**
  98. * Check whether this user is allowed to delete this category.
  99. *
  100. * @param int $todelete a category id.
  101. */
  102. function question_can_delete_cat($todelete) {
  103. global $DB;
  104. if (question_is_only_toplevel_category_in_context($todelete)) {
  105. print_error('cannotdeletecate', 'question');
  106. } else {
  107. $contextid = $DB->get_field('question_categories', 'contextid', array('id' => $todelete));
  108. require_capability('moodle/question:managecategory', context::instance_by_id($contextid));
  109. }
  110. }
  111. /**
  112. * Base class for representing a column in a {@link question_bank_view}.
  113. *
  114. * @copyright 2009 Tim Hunt
  115. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  116. * @deprecated since Moodle 2.7 MDL-40457
  117. */
  118. class_alias('core_question\bank\column_base', 'question_bank_column_base', true);
  119. /**
  120. * A column with a checkbox for each question with name q{questionid}.
  121. *
  122. * @copyright 2009 Tim Hunt
  123. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  124. * @deprecated since Moodle 2.7 MDL-40457
  125. */
  126. class_alias('core_question\bank\checkbox_column', 'question_bank_checkbox_column', true);
  127. /**
  128. * A column type for the name of the question type.
  129. *
  130. * @copyright 2009 Tim Hunt
  131. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  132. * @deprecated since Moodle 2.7 MDL-40457
  133. */
  134. class_alias('core_question\bank\question_type_column', 'question_bank_question_type_column', true);
  135. /**
  136. * A column type for the name of the question name.
  137. *
  138. * @copyright 2009 Tim Hunt
  139. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  140. * @deprecated since Moodle 2.7 MDL-40457
  141. */
  142. class_alias('core_question\bank\question_name_column', 'question_bank_question_name_column', true);
  143. /**
  144. * A column type for the name of the question creator.
  145. *
  146. * @copyright 2009 Tim Hunt
  147. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  148. * @deprecated since Moodle 2.7 MDL-40457
  149. */
  150. class_alias('core_question\bank\creator_name_column', 'question_bank_creator_name_column', true);
  151. /**
  152. * A column type for the name of the question last modifier.
  153. *
  154. * @copyright 2009 Tim Hunt
  155. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  156. * @deprecated since Moodle 2.7 MDL-40457
  157. */
  158. class_alias('core_question\bank\modifier_name_column', 'question_bank_modifier_name_column', true);
  159. /**
  160. * A base class for actions that are an icon that lets you manipulate the question in some way.
  161. *
  162. * @copyright 2009 Tim Hunt
  163. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  164. * @deprecated since Moodle 2.7 MDL-40457
  165. */
  166. class_alias('core_question\bank\action_column_base', 'question_bank_action_column_base', true);
  167. /**
  168. * Base class for question bank columns that just contain an action icon.
  169. *
  170. * @copyright 2009 Tim Hunt
  171. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  172. * @deprecated since Moodle 2.7 MDL-40457
  173. */
  174. class_alias('core_question\bank\edit_action_column', 'question_bank_edit_action_column', true);
  175. /**
  176. * Question bank column for the duplicate action icon.
  177. *
  178. * @copyright 2013 The Open University
  179. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  180. * @deprecated since Moodle 2.7 MDL-40457
  181. */
  182. class_alias('core_question\bank\copy_action_column', 'question_bank_copy_action_column', true);
  183. /**
  184. * Question bank columns for the preview action icon.
  185. *
  186. * @copyright 2009 Tim Hunt
  187. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  188. * @deprecated since Moodle 2.7 MDL-40457
  189. */
  190. class_alias('core_question\bank\preview_action_column', 'question_bank_preview_action_column', true);
  191. /**
  192. * action to delete (or hide) a question, or restore a previously hidden question.
  193. *
  194. * @copyright 2009 Tim Hunt
  195. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  196. * @deprecated since Moodle 2.7 MDL-40457
  197. */
  198. class_alias('core_question\bank\delete_action_column', 'question_bank_delete_action_column', true);
  199. /**
  200. * Base class for 'columns' that are actually displayed as a row following the main question row.
  201. *
  202. * @copyright 2009 Tim Hunt
  203. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  204. * @deprecated since Moodle 2.7 MDL-40457
  205. */
  206. class_alias('core_question\bank\row_base', 'question_bank_row_base', true);
  207. /**
  208. * A column type for the name of the question name.
  209. *
  210. * @copyright 2009 Tim Hunt
  211. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  212. * @deprecated since Moodle 2.7 MDL-40457
  213. */
  214. class_alias('core_question\bank\question_text_row', 'question_bank_question_text_row', true);
  215. /**
  216. * @copyright 2009 Tim Hunt
  217. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  218. * @deprecated since Moodle 2.7 MDL-40457
  219. */
  220. class_alias('core_question\bank\view', 'question_bank_view', true);
  221. /**
  222. * Common setup for all pages for editing questions.
  223. * @param string $baseurl the name of the script calling this funciton. For examle 'qusetion/edit.php'.
  224. * @param string $edittab code for this edit tab
  225. * @param bool $requirecmid require cmid? default false
  226. * @param bool $unused no longer used, do no pass
  227. * @return array $thispageurl, $contexts, $cmid, $cm, $module, $pagevars
  228. */
  229. function question_edit_setup($edittab, $baseurl, $requirecmid = false, $unused = null) {
  230. global $DB, $PAGE, $CFG;
  231. if ($unused !== null) {
  232. debugging('Deprecated argument passed to question_edit_setup()', DEBUG_DEVELOPER);
  233. }
  234. $thispageurl = new moodle_url($baseurl);
  235. $thispageurl->remove_all_params(); // We are going to explicity add back everything important - this avoids unwanted params from being retained.
  236. if ($requirecmid){
  237. $cmid =required_param('cmid', PARAM_INT);
  238. } else {
  239. $cmid = optional_param('cmid', 0, PARAM_INT);
  240. }
  241. if ($cmid){
  242. list($module, $cm) = get_module_from_cmid($cmid);
  243. $courseid = $cm->course;
  244. $thispageurl->params(compact('cmid'));
  245. require_login($courseid, false, $cm);
  246. $thiscontext = context_module::instance($cmid);
  247. } else {
  248. $module = null;
  249. $cm = null;
  250. $courseid = required_param('courseid', PARAM_INT);
  251. $thispageurl->params(compact('courseid'));
  252. require_login($courseid, false);
  253. $thiscontext = context_course::instance($courseid);
  254. }
  255. if ($thiscontext){
  256. $contexts = new question_edit_contexts($thiscontext);
  257. $contexts->require_one_edit_tab_cap($edittab);
  258. } else {
  259. $contexts = null;
  260. }
  261. $PAGE->set_pagelayout('admin');
  262. $pagevars['qpage'] = optional_param('qpage', -1, PARAM_INT);
  263. //pass 'cat' from page to page and when 'category' comes from a drop down menu
  264. //then we also reset the qpage so we go to page 1 of
  265. //a new cat.
  266. $pagevars['cat'] = optional_param('cat', 0, PARAM_SEQUENCE); // if empty will be set up later
  267. if ($category = optional_param('category', 0, PARAM_SEQUENCE)) {
  268. if ($pagevars['cat'] != $category) { // is this a move to a new category?
  269. $pagevars['cat'] = $category;
  270. $pagevars['qpage'] = 0;
  271. }
  272. }
  273. if ($pagevars['cat']){
  274. $thispageurl->param('cat', $pagevars['cat']);
  275. }
  276. if (strpos($baseurl, '/question/') === 0) {
  277. navigation_node::override_active_url($thispageurl);
  278. }
  279. if ($pagevars['qpage'] > -1) {
  280. $thispageurl->param('qpage', $pagevars['qpage']);
  281. } else {
  282. $pagevars['qpage'] = 0;
  283. }
  284. $pagevars['qperpage'] = question_get_display_preference(
  285. 'qperpage', DEFAULT_QUESTIONS_PER_PAGE, PARAM_INT, $thispageurl);
  286. for ($i = 1; $i <= question_bank_view::MAX_SORTS; $i++) {
  287. $param = 'qbs' . $i;
  288. if (!$sort = optional_param($param, '', PARAM_TEXT)) {
  289. break;
  290. }
  291. $thispageurl->param($param, $sort);
  292. }
  293. $defaultcategory = question_make_default_categories($contexts->all());
  294. $contextlistarr = array();
  295. foreach ($contexts->having_one_edit_tab_cap($edittab) as $context){
  296. $contextlistarr[] = "'{$context->id}'";
  297. }
  298. $contextlist = join($contextlistarr, ' ,');
  299. if (!empty($pagevars['cat'])){
  300. $catparts = explode(',', $pagevars['cat']);
  301. if (!$catparts[0] || (false !== array_search($catparts[1], $contextlistarr)) ||
  302. !$DB->count_records_select("question_categories", "id = ? AND contextid = ?", array($catparts[0], $catparts[1]))) {
  303. print_error('invalidcategory', 'question');
  304. }
  305. } else {
  306. $category = $defaultcategory;
  307. $pagevars['cat'] = "{$category->id},{$category->contextid}";
  308. }
  309. // Display options.
  310. $pagevars['recurse'] = question_get_display_preference('recurse', 1, PARAM_BOOL, $thispageurl);
  311. $pagevars['showhidden'] = question_get_display_preference('showhidden', 0, PARAM_BOOL, $thispageurl);
  312. $pagevars['qbshowtext'] = question_get_display_preference('qbshowtext', 0, PARAM_BOOL, $thispageurl);
  313. // Category list page.
  314. $pagevars['cpage'] = optional_param('cpage', 1, PARAM_INT);
  315. if ($pagevars['cpage'] != 1){
  316. $thispageurl->param('cpage', $pagevars['cpage']);
  317. }
  318. return array($thispageurl, $contexts, $cmid, $cm, $module, $pagevars);
  319. }
  320. /**
  321. * Get the category id from $pagevars.
  322. * @param array $pagevars from {@link question_edit_setup()}.
  323. * @return int the category id.
  324. */
  325. function question_get_category_id_from_pagevars(array $pagevars) {
  326. list($questioncategoryid) = explode(',', $pagevars['cat']);
  327. return $questioncategoryid;
  328. }
  329. /**
  330. * Get a particular question preference that is also stored as a user preference.
  331. * If the the value is given in the GET/POST request, then that value is used,
  332. * and the user preference is updated to that value. Otherwise, the last set
  333. * value of the user preference is used, or if it has never been set the default
  334. * passed to this function.
  335. *
  336. * @param string $param the param name. The URL parameter set, and the GET/POST
  337. * parameter read. The user_preference name is 'question_bank_' . $param.
  338. * @param mixed $default The default value to use, if not otherwise set.
  339. * @param int $type one of the PARAM_... constants.
  340. * @param moodle_url $thispageurl if the value has been explicitly set, we add
  341. * it to this URL.
  342. * @return mixed the parameter value to use.
  343. */
  344. function question_get_display_preference($param, $default, $type, $thispageurl) {
  345. $submittedvalue = optional_param($param, null, $type);
  346. if (is_null($submittedvalue)) {
  347. return get_user_preferences('question_bank_' . $param, $default);
  348. }
  349. set_user_preference('question_bank_' . $param, $submittedvalue);
  350. $thispageurl->param($param, $submittedvalue);
  351. return $submittedvalue;
  352. }
  353. /**
  354. * Make sure user is logged in as required in this context.
  355. */
  356. function require_login_in_context($contextorid = null){
  357. global $DB, $CFG;
  358. if (!is_object($contextorid)){
  359. $context = context::instance_by_id($contextorid, IGNORE_MISSING);
  360. } else {
  361. $context = $contextorid;
  362. }
  363. if ($context && ($context->contextlevel == CONTEXT_COURSE)) {
  364. require_login($context->instanceid);
  365. } else if ($context && ($context->contextlevel == CONTEXT_MODULE)) {
  366. if ($cm = $DB->get_record('course_modules',array('id' =>$context->instanceid))) {
  367. if (!$course = $DB->get_record('course', array('id' => $cm->course))) {
  368. print_error('invalidcourseid');
  369. }
  370. require_course_login($course, true, $cm);
  371. } else {
  372. print_error('invalidcoursemodule');
  373. }
  374. } else if ($context && ($context->contextlevel == CONTEXT_SYSTEM)) {
  375. if (!empty($CFG->forcelogin)) {
  376. require_login();
  377. }
  378. } else {
  379. require_login();
  380. }
  381. }
  382. /**
  383. * Print a form to let the user choose which question type to add.
  384. * When the form is submitted, it goes to the question.php script.
  385. * @param $hiddenparams hidden parameters to add to the form, in addition to
  386. * the qtype radio buttons.
  387. * @param $allowedqtypes optional list of qtypes that are allowed. If given, only
  388. * those qtypes will be shown. Example value array('description', 'multichoice').
  389. */
  390. function print_choose_qtype_to_add_form($hiddenparams, array $allowedqtypes = null, $enablejs = true) {
  391. global $CFG, $PAGE, $OUTPUT;
  392. if ($enablejs) {
  393. // Add the chooser.
  394. $PAGE->requires->yui_module('moodle-question-chooser', 'M.question.init_chooser', array(array()));
  395. }
  396. $realqtypes = array();
  397. $fakeqtypes = array();
  398. foreach (question_bank::get_creatable_qtypes() as $qtypename => $qtype) {
  399. if ($allowedqtypes && !in_array($qtypename, $allowedqtypes)) {
  400. continue;
  401. }
  402. if ($qtype->is_real_question_type()) {
  403. $realqtypes[] = $qtype;
  404. } else {
  405. $fakeqtypes[] = $qtype;
  406. }
  407. }
  408. $renderer = $PAGE->get_renderer('question', 'bank');
  409. return $renderer->qbank_chooser($realqtypes, $fakeqtypes, $PAGE->course, $hiddenparams);
  410. }
  411. /**
  412. * Print a button for creating a new question. This will open question/addquestion.php,
  413. * which in turn goes to question/question.php before getting back to $params['returnurl']
  414. * (by default the question bank screen).
  415. *
  416. * @param int $categoryid The id of the category that the new question should be added to.
  417. * @param array $params Other paramters to add to the URL. You need either $params['cmid'] or
  418. * $params['courseid'], and you should probably set $params['returnurl']
  419. * @param string $caption the text to display on the button.
  420. * @param string $tooltip a tooltip to add to the button (optional).
  421. * @param bool $disabled if true, the button will be disabled.
  422. */
  423. function create_new_question_button($categoryid, $params, $caption, $tooltip = '', $disabled = false) {
  424. global $CFG, $PAGE, $OUTPUT;
  425. static $choiceformprinted = false;
  426. $params['category'] = $categoryid;
  427. $url = new moodle_url('/question/addquestion.php', $params);
  428. echo $OUTPUT->single_button($url, $caption, 'get', array('disabled'=>$disabled, 'title'=>$tooltip));
  429. if (!$choiceformprinted) {
  430. echo '<div id="qtypechoicecontainer">';
  431. echo print_choose_qtype_to_add_form(array());
  432. echo "</div>\n";
  433. $choiceformprinted = true;
  434. }
  435. }