Merge branch 'MDL-70326-MOODLE_310_STABLE' of https://github.com/durzo/moodle into...
[moodle.git] / question / preview.php
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/>.
17 /**
18  * This page displays a preview of a question
19  *
20  * The preview uses the option settings from the activity within which the question
21  * is previewed or the default settings if no activity is specified. The question session
22  * information is stored in the session as an array of subsequent states rather
23  * than in the database.
24  *
25  * @package    moodlecore
26  * @subpackage questionengine
27  * @copyright  Alex Smith {@link http://maths.york.ac.uk/serving_maths} and
28  *      numerous contributors.
29  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
30  */
33 require_once(__DIR__ . '/../config.php');
34 require_once($CFG->libdir . '/questionlib.php');
35 require_once(__DIR__ . '/previewlib.php');
37 /**
38  * The maximum number of variants previewable. If there are more variants than this for a question
39  * then we only allow the selection of the first x variants.
40  * @var integer
41  */
42 define('QUESTION_PREVIEW_MAX_VARIANTS', 100);
44 // Get and validate question id.
45 $id = required_param('id', PARAM_INT);
46 $question = question_bank::load_question($id);
48 // Were we given a particular context to run the question in?
49 // This affects things like filter settings, or forced theme or language.
50 if ($cmid = optional_param('cmid', 0, PARAM_INT)) {
51     $cm = get_coursemodule_from_id(false, $cmid);
52     require_login($cm->course, false, $cm);
53     $context = context_module::instance($cmid);
55 } else if ($courseid = optional_param('courseid', 0, PARAM_INT)) {
56     require_login($courseid);
57     $context = context_course::instance($courseid);
59 } else {
60     require_login();
61     $category = $DB->get_record('question_categories',
62             array('id' => $question->category), '*', MUST_EXIST);
63     $context = context::instance_by_id($category->contextid);
64     $PAGE->set_context($context);
65     // Note that in the other cases, require_login will set the correct page context.
66 }
67 question_require_capability_on($question, 'use');
68 $PAGE->set_pagelayout('popup');
70 // Get and validate display options.
71 $maxvariant = min($question->get_num_variants(), QUESTION_PREVIEW_MAX_VARIANTS);
72 $options = new question_preview_options($question);
73 $options->load_user_defaults();
74 $options->set_from_request();
75 $PAGE->set_url(question_preview_url($id, $options->behaviour, $options->maxmark,
76         $options, $options->variant, $context));
78 // Get and validate existing preview, or start a new one.
79 $previewid = optional_param('previewid', 0, PARAM_INT);
81 if ($previewid) {
82     try {
83         $quba = question_engine::load_questions_usage_by_activity($previewid);
85     } catch (Exception $e) {
86         // This may not seem like the right error message to display, but
87         // actually from the user point of view, it makes sense.
88         print_error('submissionoutofsequencefriendlymessage', 'question',
89                 question_preview_url($question->id, $options->behaviour,
90                 $options->maxmark, $options, $options->variant, $context), null, $e);
91     }
93     if ($quba->get_owning_context()->instanceid != $USER->id) {
94         print_error('notyourpreview', 'question');
95     }
97     $slot = $quba->get_first_question_number();
98     $usedquestion = $quba->get_question($slot, false);
99     if ($usedquestion->id != $question->id) {
100         print_error('questionidmismatch', 'question');
101     }
102     $question = $usedquestion;
103     $options->variant = $quba->get_variant($slot);
105 } else {
106     $quba = question_engine::make_questions_usage_by_activity(
107             'core_question_preview', context_user::instance($USER->id));
108     $quba->set_preferred_behaviour($options->behaviour);
109     $slot = $quba->add_question($question, $options->maxmark);
111     if ($options->variant) {
112         $options->variant = min($maxvariant, max(1, $options->variant));
113     } else {
114         $options->variant = rand(1, $maxvariant);
115     }
117     $quba->start_question($slot, $options->variant);
119     $transaction = $DB->start_delegated_transaction();
120     question_engine::save_questions_usage_by_activity($quba);
121     $transaction->allow_commit();
123 $options->behaviour = $quba->get_preferred_behaviour();
124 $options->maxmark = $quba->get_question_max_mark($slot);
126 // Create the settings form, and initialise the fields.
127 $optionsform = new preview_options_form(question_preview_form_url($question->id, $context, $previewid),
128         array('quba' => $quba, 'maxvariant' => $maxvariant));
129 $optionsform->set_data($options);
131 // Process change of settings, if that was requested.
132 if ($newoptions = $optionsform->get_submitted_data()) {
133     // Set user preferences.
134     $options->save_user_preview_options($newoptions);
135     if (!isset($newoptions->variant)) {
136         $newoptions->variant = $options->variant;
137     }
138     if (isset($newoptions->saverestart)) {
139         restart_preview($previewid, $question->id, $newoptions, $context);
140     }
143 // Prepare a URL that is used in various places.
144 $actionurl = question_preview_action_url($question->id, $quba->get_id(), $options, $context);
146 // Process any actions from the buttons at the bottom of the form.
147 if (data_submitted() && confirm_sesskey()) {
149     try {
151         if (optional_param('restart', false, PARAM_BOOL)) {
152             restart_preview($previewid, $question->id, $options, $context);
154         } else if (optional_param('fill', null, PARAM_BOOL)) {
155             $correctresponse = $quba->get_correct_response($slot);
156             if (!is_null($correctresponse)) {
157                 $quba->process_action($slot, $correctresponse);
159                 $transaction = $DB->start_delegated_transaction();
160                 question_engine::save_questions_usage_by_activity($quba);
161                 $transaction->allow_commit();
162             }
163             redirect($actionurl);
165         } else if (optional_param('finish', null, PARAM_BOOL)) {
166             $quba->process_all_actions();
167             $quba->finish_all_questions();
169             $transaction = $DB->start_delegated_transaction();
170             question_engine::save_questions_usage_by_activity($quba);
171             $transaction->allow_commit();
172             redirect($actionurl);
174         } else {
175             $quba->process_all_actions();
177             $transaction = $DB->start_delegated_transaction();
178             question_engine::save_questions_usage_by_activity($quba);
179             $transaction->allow_commit();
181             $scrollpos = optional_param('scrollpos', '', PARAM_RAW);
182             if ($scrollpos !== '') {
183                 $actionurl->param('scrollpos', (int) $scrollpos);
184             }
185             redirect($actionurl);
186         }
188     } catch (question_out_of_sequence_exception $e) {
189         print_error('submissionoutofsequencefriendlymessage', 'question', $actionurl);
191     } catch (Exception $e) {
192         // This sucks, if we display our own custom error message, there is no way
193         // to display the original stack trace.
194         $debuginfo = '';
195         if (!empty($e->debuginfo)) {
196             $debuginfo = $e->debuginfo;
197         }
198         print_error('errorprocessingresponses', 'question', $actionurl,
199                 $e->getMessage(), $debuginfo);
200     }
203 if ($question->length) {
204     $displaynumber = '1';
205 } else {
206     $displaynumber = 'i';
208 $restartdisabled = array();
209 $finishdisabled = array();
210 $filldisabled = array();
211 if ($quba->get_question_state($slot)->is_finished()) {
212     $finishdisabled = array('disabled' => 'disabled');
213     $filldisabled = array('disabled' => 'disabled');
215 // If question type cannot give us a correct response, disable this button.
216 if (is_null($quba->get_correct_response($slot))) {
217     $filldisabled = array('disabled' => 'disabled');
219 if (!$previewid) {
220     $restartdisabled = array('disabled' => 'disabled');
223 // Prepare technical info to be output.
224 $qa = $quba->get_question_attempt($slot);
225 $technical = array();
226 $technical[] = get_string('behaviourbeingused', 'question',
227         question_engine::get_behaviour_name($qa->get_behaviour_name()));
228 $technical[] = get_string('technicalinfominfraction',     'question', $qa->get_min_fraction());
229 $technical[] = get_string('technicalinfomaxfraction',     'question', $qa->get_max_fraction());
230 $technical[] = get_string('technicalinfovariant',         'question', $qa->get_variant());
231 $technical[] = get_string('technicalinfoquestionsummary', 'question', s($qa->get_question_summary()));
232 $technical[] = get_string('technicalinforightsummary',    'question', s($qa->get_right_answer_summary()));
233 $technical[] = get_string('technicalinforesponsesummary', 'question', s($qa->get_response_summary()));
234 $technical[] = get_string('technicalinfostate',           'question', '' . $qa->get_state());
236 // Start output.
237 $title = get_string('previewquestion', 'question', format_string($question->name));
238 $headtags = question_engine::initialise_js() . $quba->render_question_head_html($slot);
239 $PAGE->set_title($title);
240 $PAGE->set_heading($title);
241 echo $OUTPUT->header();
243 // Start the question form.
244 echo html_writer::start_tag('form', array('method' => 'post', 'action' => $actionurl,
245         'enctype' => 'multipart/form-data', 'id' => 'responseform'));
246 echo html_writer::start_tag('div');
247 echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey()));
248 echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'slots', 'value' => $slot));
249 echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'scrollpos', 'value' => '', 'id' => 'scrollpos'));
250 echo html_writer::end_tag('div');
252 // Output the question.
253 echo $quba->render_question($slot, $options, $displaynumber);
255 // Finish the question form.
256 echo html_writer::start_tag('div', array('id' => 'previewcontrols', 'class' => 'controls'));
257 echo html_writer::empty_tag('input', $restartdisabled + array('type' => 'submit',
258         'name' => 'restart', 'value' => get_string('restart', 'question'), 'class' => 'btn btn-secondary'));
259 echo html_writer::empty_tag('input', $finishdisabled  + array('type' => 'submit',
260         'name' => 'save',    'value' => get_string('save', 'question'), 'class' => 'btn btn-secondary'));
261 echo html_writer::empty_tag('input', $filldisabled    + array('type' => 'submit',
262         'name' => 'fill',    'value' => get_string('fillincorrect', 'question'), 'class' => 'btn btn-secondary'));
263 echo html_writer::empty_tag('input', $finishdisabled  + array('type' => 'submit',
264         'name' => 'finish',  'value' => get_string('submitandfinish', 'question'), 'class' => 'btn btn-secondary'));
265 echo html_writer::end_tag('div');
266 echo html_writer::end_tag('form');
268 // Output the technical info.
269 print_collapsible_region_start('', 'techinfo', get_string('technicalinfo', 'question'),
270         'core_question_preview_techinfo_collapsed', true, false, $OUTPUT->help_icon('technicalinfo', 'question'));
271 foreach ($technical as $info) {
272     echo html_writer::tag('p', $info, array('class' => 'notifytiny'));
274 print_collapsible_region_end();
276 // Output a link to export this single question.
277 if (question_has_capability_on($question, 'view')) {
278     echo html_writer::link(question_get_export_single_question_url($question),
279             get_string('exportonequestion', 'question'));
282 // Log the preview of this question.
283 $event = \core\event\question_viewed::create_from_question_instance($question, $context);
284 $event->trigger();
286 // Display the settings form.
287 $optionsform->display();
289 $PAGE->requires->js_module('core_question_engine');
290 $PAGE->requires->strings_for_js(array(
291     'closepreview',
292 ), 'question');
293 $PAGE->requires->yui_module('moodle-question-preview', 'M.question.preview.init');
294 echo $OUTPUT->footer();