previewlib.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  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. * Library functions used by question/preview.php.
  18. *
  19. * @package moodlecore
  20. * @subpackage questionengine
  21. * @copyright 2010 The Open University
  22. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23. */
  24. defined('MOODLE_INTERNAL') || die();
  25. require_once($CFG->libdir . '/formslib.php');
  26. /**
  27. * Settings form for the preview options.
  28. *
  29. * @copyright 2009 The Open University
  30. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  31. */
  32. class preview_options_form extends moodleform {
  33. public function definition() {
  34. $mform = $this->_form;
  35. $hiddenofvisible = array(
  36. question_display_options::HIDDEN => get_string('notshown', 'question'),
  37. question_display_options::VISIBLE => get_string('shown', 'question'),
  38. );
  39. $mform->addElement('header', 'optionsheader', get_string('attemptoptions', 'question'));
  40. $behaviours = question_engine::get_behaviour_options(
  41. $this->_customdata['quba']->get_preferred_behaviour());
  42. $mform->addElement('select', 'behaviour',
  43. get_string('howquestionsbehave', 'question'), $behaviours);
  44. $mform->addHelpButton('behaviour', 'howquestionsbehave', 'question');
  45. $mform->addElement('text', 'maxmark', get_string('markedoutof', 'question'),
  46. array('size' => '5'));
  47. $mform->setType('maxmark', PARAM_FLOAT);
  48. if ($this->_customdata['maxvariant'] > 1) {
  49. $variants = range(1, $this->_customdata['maxvariant']);
  50. $mform->addElement('select', 'variant', get_string('questionvariant', 'question'),
  51. array_combine($variants, $variants));
  52. }
  53. $mform->setType('variant', PARAM_INT);
  54. $mform->addElement('submit', 'saverestart',
  55. get_string('restartwiththeseoptions', 'question'));
  56. $mform->addElement('header', 'optionsheader', get_string('displayoptions', 'question'));
  57. $mform->addElement('select', 'correctness', get_string('whethercorrect', 'question'),
  58. $hiddenofvisible);
  59. $marksoptions = array(
  60. question_display_options::HIDDEN => get_string('notshown', 'question'),
  61. question_display_options::MAX_ONLY => get_string('showmaxmarkonly', 'question'),
  62. question_display_options::MARK_AND_MAX => get_string('showmarkandmax', 'question'),
  63. );
  64. $mform->addElement('select', 'marks', get_string('marks', 'question'), $marksoptions);
  65. $mform->addElement('select', 'markdp', get_string('decimalplacesingrades', 'question'),
  66. question_engine::get_dp_options());
  67. $mform->addElement('select', 'feedback',
  68. get_string('specificfeedback', 'question'), $hiddenofvisible);
  69. $mform->addElement('select', 'generalfeedback',
  70. get_string('generalfeedback', 'question'), $hiddenofvisible);
  71. $mform->addElement('select', 'rightanswer',
  72. get_string('rightanswer', 'question'), $hiddenofvisible);
  73. $mform->addElement('select', 'history',
  74. get_string('responsehistory', 'question'), $hiddenofvisible);
  75. $mform->addElement('submit', 'saveupdate',
  76. get_string('updatedisplayoptions', 'question'));
  77. }
  78. }
  79. /**
  80. * Displays question preview options as default and set the options
  81. * Setting default, getting and setting user preferences in question preview options.
  82. *
  83. * @copyright 2010 The Open University
  84. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  85. */
  86. class question_preview_options extends question_display_options {
  87. /** @var string the behaviour to use for this preview. */
  88. public $behaviour;
  89. /** @var number the maximum mark to use for this preview. */
  90. public $maxmark;
  91. /** @var int the variant of the question to preview. */
  92. public $variant;
  93. /** @var string prefix to append to field names to get user_preference names. */
  94. const OPTIONPREFIX = 'question_preview_options_';
  95. /**
  96. * Constructor.
  97. */
  98. public function __construct($question) {
  99. $this->behaviour = 'deferredfeedback';
  100. $this->maxmark = $question->defaultmark;
  101. $this->variant = null;
  102. $this->correctness = self::VISIBLE;
  103. $this->marks = self::MARK_AND_MAX;
  104. $this->markdp = get_config('quiz', 'decimalpoints');
  105. $this->feedback = self::VISIBLE;
  106. $this->numpartscorrect = $this->feedback;
  107. $this->generalfeedback = self::VISIBLE;
  108. $this->rightanswer = self::VISIBLE;
  109. $this->history = self::HIDDEN;
  110. $this->flags = self::HIDDEN;
  111. $this->manualcomment = self::HIDDEN;
  112. }
  113. /**
  114. * @return array names of the options we store in the user preferences table.
  115. */
  116. protected function get_user_pref_fields() {
  117. return array('behaviour', 'correctness', 'marks', 'markdp', 'feedback',
  118. 'generalfeedback', 'rightanswer', 'history');
  119. }
  120. /**
  121. * @return array names and param types of the options we read from the request.
  122. */
  123. protected function get_field_types() {
  124. return array(
  125. 'behaviour' => PARAM_ALPHA,
  126. 'maxmark' => PARAM_FLOAT,
  127. 'variant' => PARAM_INT,
  128. 'correctness' => PARAM_BOOL,
  129. 'marks' => PARAM_INT,
  130. 'markdp' => PARAM_INT,
  131. 'feedback' => PARAM_BOOL,
  132. 'generalfeedback' => PARAM_BOOL,
  133. 'rightanswer' => PARAM_BOOL,
  134. 'history' => PARAM_BOOL,
  135. );
  136. }
  137. /**
  138. * Load the value of the options from the user_preferences table.
  139. */
  140. public function load_user_defaults() {
  141. $defaults = get_config('question_preview');
  142. foreach ($this->get_user_pref_fields() as $field) {
  143. $this->$field = get_user_preferences(
  144. self::OPTIONPREFIX . $field, $defaults->$field);
  145. }
  146. $this->numpartscorrect = $this->feedback;
  147. }
  148. /**
  149. * Save a change to the user's preview options to the database.
  150. * @param object $newoptions
  151. */
  152. public function save_user_preview_options($newoptions) {
  153. foreach ($this->get_user_pref_fields() as $field) {
  154. if (isset($newoptions->$field)) {
  155. set_user_preference(self::OPTIONPREFIX . $field, $newoptions->$field);
  156. }
  157. }
  158. }
  159. /**
  160. * Set the value of any fields included in the request.
  161. */
  162. public function set_from_request() {
  163. foreach ($this->get_field_types() as $field => $type) {
  164. $this->$field = optional_param($field, $this->$field, $type);
  165. }
  166. $this->numpartscorrect = $this->feedback;
  167. }
  168. /**
  169. * @return string URL fragment. Parameters needed in the URL when continuing
  170. * this preview.
  171. */
  172. public function get_url_params() {
  173. $params = array();
  174. foreach ($this->get_field_types() as $field => $notused) {
  175. if ($field == 'behaviour' || $field == 'maxmark' || is_null($this->$field)) {
  176. continue;
  177. }
  178. $params[$field] = $this->$field;
  179. }
  180. return $params;
  181. }
  182. }
  183. /**
  184. * Called via pluginfile.php -> question_pluginfile to serve files belonging to
  185. * a question in a question_attempt when that attempt is a preview.
  186. *
  187. * @package core_question
  188. * @category files
  189. * @param stdClass $course course settings object
  190. * @param stdClass $context context object
  191. * @param string $component the name of the component we are serving files for.
  192. * @param string $filearea the name of the file area.
  193. * @param int $qubaid the question_usage this image belongs to.
  194. * @param int $slot the relevant slot within the usage.
  195. * @param array $args the remaining bits of the file path.
  196. * @param bool $forcedownload whether the user must be forced to download the file.
  197. * @param array $options additional options affecting the file serving
  198. * @return bool false if file not found, does not return if found - justsend the file
  199. */
  200. function question_preview_question_pluginfile($course, $context, $component,
  201. $filearea, $qubaid, $slot, $args, $forcedownload, $fileoptions) {
  202. global $USER, $DB, $CFG;
  203. list($context, $course, $cm) = get_context_info_array($context->id);
  204. require_login($course, false, $cm);
  205. $quba = question_engine::load_questions_usage_by_activity($qubaid);
  206. if (!question_has_capability_on($quba->get_question($slot), 'use')) {
  207. send_file_not_found();
  208. }
  209. $options = new question_display_options();
  210. $options->feedback = question_display_options::VISIBLE;
  211. $options->numpartscorrect = question_display_options::VISIBLE;
  212. $options->generalfeedback = question_display_options::VISIBLE;
  213. $options->rightanswer = question_display_options::VISIBLE;
  214. $options->manualcomment = question_display_options::VISIBLE;
  215. $options->history = question_display_options::VISIBLE;
  216. if (!$quba->check_file_access($slot, $options, $component,
  217. $filearea, $args, $forcedownload)) {
  218. send_file_not_found();
  219. }
  220. $fs = get_file_storage();
  221. $relativepath = implode('/', $args);
  222. $fullpath = "/{$context->id}/{$component}/{$filearea}/{$relativepath}";
  223. if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) {
  224. send_file_not_found();
  225. }
  226. send_stored_file($file, 0, 0, $forcedownload, $fileoptions);
  227. }
  228. /**
  229. * The the URL to use for actions relating to this preview.
  230. * @param int $questionid the question being previewed.
  231. * @param int $qubaid the id of the question usage for this preview.
  232. * @param question_preview_options $options the options in use.
  233. */
  234. function question_preview_action_url($questionid, $qubaid,
  235. question_preview_options $options, $context) {
  236. $params = array(
  237. 'id' => $questionid,
  238. 'previewid' => $qubaid,
  239. );
  240. if ($context->contextlevel == CONTEXT_MODULE) {
  241. $params['cmid'] = $context->instanceid;
  242. } else if ($context->contextlevel == CONTEXT_COURSE) {
  243. $params['courseid'] = $context->instanceid;
  244. }
  245. $params = array_merge($params, $options->get_url_params());
  246. return new moodle_url('/question/preview.php', $params);
  247. }
  248. /**
  249. * The the URL to use for actions relating to this preview.
  250. * @param int $questionid the question being previewed.
  251. * @param context $context the current moodle context.
  252. * @param int $previewid optional previewid to sign post saved previewed answers.
  253. */
  254. function question_preview_form_url($questionid, $context, $previewid = null) {
  255. $params = array(
  256. 'id' => $questionid,
  257. );
  258. if ($context->contextlevel == CONTEXT_MODULE) {
  259. $params['cmid'] = $context->instanceid;
  260. } else if ($context->contextlevel == CONTEXT_COURSE) {
  261. $params['courseid'] = $context->instanceid;
  262. }
  263. if ($previewid) {
  264. $params['previewid'] = $previewid;
  265. }
  266. return new moodle_url('/question/preview.php', $params);
  267. }
  268. /**
  269. * Delete the current preview, if any, and redirect to start a new preview.
  270. * @param int $previewid
  271. * @param int $questionid
  272. * @param object $displayoptions
  273. * @param object $context
  274. */
  275. function restart_preview($previewid, $questionid, $displayoptions, $context) {
  276. global $DB;
  277. if ($previewid) {
  278. $transaction = $DB->start_delegated_transaction();
  279. question_engine::delete_questions_usage_by_activity($previewid);
  280. $transaction->allow_commit();
  281. }
  282. redirect(question_preview_url($questionid, $displayoptions->behaviour,
  283. $displayoptions->maxmark, $displayoptions, $displayoptions->variant, $context));
  284. }
  285. /**
  286. * Scheduled tasks relating to question preview. Specifically, delete any old
  287. * previews that are left over in the database.
  288. */
  289. function question_preview_cron() {
  290. $maxage = 24*60*60; // We delete previews that have not been touched for 24 hours.
  291. $lastmodifiedcutoff = time() - $maxage;
  292. mtrace("\n Cleaning up old question previews...", '');
  293. $oldpreviews = new qubaid_join('{question_usages} quba', 'quba.id',
  294. 'quba.component = :qubacomponent
  295. AND NOT EXISTS (
  296. SELECT 1
  297. FROM {question_attempts} subq_qa
  298. JOIN {question_attempt_steps} subq_qas ON subq_qas.questionattemptid = subq_qa.id
  299. JOIN {question_usages} subq_qu ON subq_qu.id = subq_qa.questionusageid
  300. WHERE subq_qa.questionusageid = quba.id
  301. AND subq_qu.component = :qubacomponent2
  302. AND (subq_qa.timemodified > :qamodifiedcutoff
  303. OR subq_qas.timecreated > :stepcreatedcutoff)
  304. )
  305. ',
  306. array('qubacomponent' => 'core_question_preview', 'qubacomponent2' => 'core_question_preview',
  307. 'qamodifiedcutoff' => $lastmodifiedcutoff, 'stepcreatedcutoff' => $lastmodifiedcutoff));
  308. question_engine::delete_questions_usage_by_activities($oldpreviews);
  309. mtrace('done.');
  310. }