MDL-44180 mod_quiz: unified @package use
[moodle.git] / mod / quiz / edit.php
blobb6cd5b9a93bcabfc8832345012401b633bd0093e
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.
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/>.
18 /**
19 * Page to edit quizzes
21 * This page generally has two columns:
22 * The right column lists all available questions in a chosen category and
23 * allows them to be edited or more to be added. This column is only there if
24 * the quiz does not already have student attempts
25 * The left column lists all questions that have been added to the current quiz.
26 * The lecturer can add questions from the right hand list to the quiz or remove them
28 * The script also processes a number of actions:
29 * Actions affecting a quiz:
30 * up and down Changes the order of questions and page breaks
31 * addquestion Adds a single question to the quiz
32 * add Adds several selected questions to the quiz
33 * addrandom Adds a certain number of random questions to the quiz
34 * repaginate Re-paginates the quiz
35 * delete Removes a question from the quiz
36 * savechanges Saves the order and grades for questions in the quiz
38 * @package mod_quiz
39 * @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com}
40 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
44 require_once('../../config.php');
45 require_once($CFG->dirroot . '/mod/quiz/editlib.php');
46 require_once($CFG->dirroot . '/mod/quiz/addrandomform.php');
47 require_once($CFG->dirroot . '/question/category_class.php');
50 /**
51 * Callback function called from question_list() function
52 * (which is called from showbank())
53 * Displays button in form with checkboxes for each question.
55 function module_specific_buttons($cmid, $cmoptions) {
56 global $OUTPUT;
57 $params = array(
58 'type' => 'submit',
59 'name' => 'add',
60 'value' => $OUTPUT->larrow() . ' ' . get_string('addtoquiz', 'quiz'),
62 if ($cmoptions->hasattempts) {
63 $params['disabled'] = 'disabled';
65 return html_writer::empty_tag('input', $params);
68 /**
69 * Callback function called from question_list() function
70 * (which is called from showbank())
72 function module_specific_controls($totalnumber, $recurse, $category, $cmid, $cmoptions) {
73 global $OUTPUT;
74 $out = '';
75 $catcontext = context::instance_by_id($category->contextid);
76 if (has_capability('moodle/question:useall', $catcontext)) {
77 if ($cmoptions->hasattempts) {
78 $disabled = ' disabled="disabled"';
79 } else {
80 $disabled = '';
82 $randomusablequestions =
83 question_bank::get_qtype('random')->get_available_questions_from_category(
84 $category->id, $recurse);
85 $maxrand = count($randomusablequestions);
86 if ($maxrand > 0) {
87 for ($i = 1; $i <= min(10, $maxrand); $i++) {
88 $randomcount[$i] = $i;
90 for ($i = 20; $i <= min(100, $maxrand); $i += 10) {
91 $randomcount[$i] = $i;
93 } else {
94 $randomcount[0] = 0;
95 $disabled = ' disabled="disabled"';
98 $out = '<strong><label for="menurandomcount">'.get_string('addrandomfromcategory', 'quiz').
99 '</label></strong><br />';
100 $attributes = array();
101 $attributes['disabled'] = $disabled ? 'disabled' : null;
102 $select = html_writer::select($randomcount, 'randomcount', '1', null, $attributes);
103 $out .= get_string('addrandom', 'quiz', $select);
104 $out .= '<input type="hidden" name="recurse" value="'.$recurse.'" />';
105 $out .= '<input type="hidden" name="categoryid" value="' . $category->id . '" />';
106 $out .= ' <input type="submit" name="addrandom" value="'.
107 get_string('addtoquiz', 'quiz').'"' . $disabled . ' />';
108 $out .= $OUTPUT->help_icon('addarandomquestion', 'quiz');
110 return $out;
113 // These params are only passed from page request to request while we stay on
114 // this page otherwise they would go in question_edit_setup.
115 $quiz_reordertool = optional_param('reordertool', -1, PARAM_BOOL);
116 $quiz_qbanktool = optional_param('qbanktool', -1, PARAM_BOOL);
117 $scrollpos = optional_param('scrollpos', '', PARAM_INT);
119 list($thispageurl, $contexts, $cmid, $cm, $quiz, $pagevars) =
120 question_edit_setup('editq', '/mod/quiz/edit.php', true);
121 $quiz->questions = quiz_clean_layout($quiz->questions);
123 $defaultcategoryobj = question_make_default_categories($contexts->all());
124 $defaultcategory = $defaultcategoryobj->id . ',' . $defaultcategoryobj->contextid;
126 if ($quiz_qbanktool > -1) {
127 $thispageurl->param('qbanktool', $quiz_qbanktool);
128 set_user_preference('quiz_qbanktool_open', $quiz_qbanktool);
129 } else {
130 $quiz_qbanktool = get_user_preferences('quiz_qbanktool_open', 0);
133 if ($quiz_reordertool > -1) {
134 $thispageurl->param('reordertool', $quiz_reordertool);
135 set_user_preference('quiz_reordertab', $quiz_reordertool);
136 } else {
137 $quiz_reordertool = get_user_preferences('quiz_reordertab', 0);
140 $canaddrandom = $contexts->have_cap('moodle/question:useall');
141 $canaddquestion = (bool) $contexts->having_add_and_use();
143 $quizhasattempts = quiz_has_attempts($quiz->id);
145 $PAGE->set_url($thispageurl);
147 // Get the course object and related bits.
148 $course = $DB->get_record('course', array('id' => $quiz->course));
149 if (!$course) {
150 print_error('invalidcourseid', 'error');
153 $questionbank = new quiz_question_bank_view($contexts, $thispageurl, $course, $cm, $quiz);
154 $questionbank->set_quiz_has_attempts($quizhasattempts);
156 // Log this visit.
157 add_to_log($cm->course, 'quiz', 'editquestions',
158 "view.php?id=$cm->id", "$quiz->id", $cm->id);
160 // You need mod/quiz:manage in addition to question capabilities to access this page.
161 require_capability('mod/quiz:manage', $contexts->lowest());
163 if (empty($quiz->grades)) {
164 $quiz->grades = quiz_get_all_question_grades($quiz);
167 // Process commands ============================================================
168 if ($quiz->shufflequestions) {
169 // Strip page breaks before processing actions, so that re-ordering works
170 // as expected when shuffle questions is on.
171 $quiz->questions = quiz_repaginate($quiz->questions, 0);
174 // Get the list of question ids had their check-boxes ticked.
175 $selectedquestionids = array();
176 $params = (array) data_submitted();
177 foreach ($params as $key => $value) {
178 if (preg_match('!^s([0-9]+)$!', $key, $matches)) {
179 $selectedquestionids[] = $matches[1];
183 $afteractionurl = new moodle_url($thispageurl);
184 if ($scrollpos) {
185 $afteractionurl->param('scrollpos', $scrollpos);
187 if (($up = optional_param('up', false, PARAM_INT)) && confirm_sesskey()) {
188 $quiz->questions = quiz_move_question_up($quiz->questions, $up);
189 $DB->set_field('quiz', 'questions', $quiz->questions, array('id' => $quiz->id));
190 quiz_delete_previews($quiz);
191 redirect($afteractionurl);
194 if (($down = optional_param('down', false, PARAM_INT)) && confirm_sesskey()) {
195 $quiz->questions = quiz_move_question_down($quiz->questions, $down);
196 $DB->set_field('quiz', 'questions', $quiz->questions, array('id' => $quiz->id));
197 quiz_delete_previews($quiz);
198 redirect($afteractionurl);
201 if (optional_param('repaginate', false, PARAM_BOOL) && confirm_sesskey()) {
202 // Re-paginate the quiz.
203 $questionsperpage = optional_param('questionsperpage', $quiz->questionsperpage, PARAM_INT);
204 $quiz->questions = quiz_repaginate($quiz->questions, $questionsperpage );
205 $DB->set_field('quiz', 'questions', $quiz->questions, array('id' => $quiz->id));
206 quiz_delete_previews($quiz);
207 redirect($afteractionurl);
210 if (($addquestion = optional_param('addquestion', 0, PARAM_INT)) && confirm_sesskey()) {
211 // Add a single question to the current quiz.
212 quiz_require_question_use($addquestion);
213 $addonpage = optional_param('addonpage', 0, PARAM_INT);
214 quiz_add_quiz_question($addquestion, $quiz, $addonpage);
215 quiz_delete_previews($quiz);
216 quiz_update_sumgrades($quiz);
217 $thispageurl->param('lastchanged', $addquestion);
218 redirect($afteractionurl);
221 if (optional_param('add', false, PARAM_BOOL) && confirm_sesskey()) {
222 // Add selected questions to the current quiz.
223 $rawdata = (array) data_submitted();
224 foreach ($rawdata as $key => $value) { // Parse input for question ids.
225 if (preg_match('!^q([0-9]+)$!', $key, $matches)) {
226 $key = $matches[1];
227 quiz_require_question_use($key);
228 quiz_add_quiz_question($key, $quiz);
231 quiz_delete_previews($quiz);
232 quiz_update_sumgrades($quiz);
233 redirect($afteractionurl);
236 if ((optional_param('addrandom', false, PARAM_BOOL)) && confirm_sesskey()) {
237 // Add random questions to the quiz.
238 $recurse = optional_param('recurse', 0, PARAM_BOOL);
239 $addonpage = optional_param('addonpage', 0, PARAM_INT);
240 $categoryid = required_param('categoryid', PARAM_INT);
241 $randomcount = required_param('randomcount', PARAM_INT);
242 quiz_add_random_questions($quiz, $addonpage, $categoryid, $randomcount, $recurse);
244 quiz_delete_previews($quiz);
245 quiz_update_sumgrades($quiz);
246 redirect($afteractionurl);
249 if (optional_param('addnewpagesafterselected', null, PARAM_CLEAN) &&
250 !empty($selectedquestionids) && confirm_sesskey()) {
251 foreach ($selectedquestionids as $questionid) {
252 $quiz->questions = quiz_add_page_break_after($quiz->questions, $questionid);
254 $DB->set_field('quiz', 'questions', $quiz->questions, array('id' => $quiz->id));
255 quiz_delete_previews($quiz);
256 redirect($afteractionurl);
259 $addpage = optional_param('addpage', false, PARAM_INT);
260 if ($addpage !== false && confirm_sesskey()) {
261 $quiz->questions = quiz_add_page_break_at($quiz->questions, $addpage);
262 $DB->set_field('quiz', 'questions', $quiz->questions, array('id' => $quiz->id));
263 quiz_delete_previews($quiz);
264 redirect($afteractionurl);
267 $deleteemptypage = optional_param('deleteemptypage', false, PARAM_INT);
268 if (($deleteemptypage !== false) && confirm_sesskey()) {
269 $quiz->questions = quiz_delete_empty_page($quiz->questions, $deleteemptypage);
270 $DB->set_field('quiz', 'questions', $quiz->questions, array('id' => $quiz->id));
271 quiz_delete_previews($quiz);
272 redirect($afteractionurl);
275 $remove = optional_param('remove', false, PARAM_INT);
276 if ($remove && confirm_sesskey()) {
277 // Remove a question from the quiz.
278 // We require the user to have the 'use' capability on the question,
279 // so that then can add it back if they remove the wrong one by mistake,
280 // but, if the question is missing, it can always be removed.
281 if ($DB->record_exists('question', array('id' => $remove))) {
282 quiz_require_question_use($remove);
284 quiz_remove_question($quiz, $remove);
285 quiz_delete_previews($quiz);
286 quiz_update_sumgrades($quiz);
287 redirect($afteractionurl);
290 if (optional_param('quizdeleteselected', false, PARAM_BOOL) &&
291 !empty($selectedquestionids) && confirm_sesskey()) {
292 foreach ($selectedquestionids as $questionid) {
293 if (quiz_has_question_use($questionid)) {
294 quiz_remove_question($quiz, $questionid);
297 quiz_delete_previews($quiz);
298 quiz_update_sumgrades($quiz);
299 redirect($afteractionurl);
302 if (optional_param('savechanges', false, PARAM_BOOL) && confirm_sesskey()) {
303 $deletepreviews = false;
304 $recomputesummarks = false;
306 $oldquestions = explode(',', $quiz->questions); // The questions in the old order.
307 $questions = array(); // For questions in the new order.
308 $rawdata = (array) data_submitted();
309 $moveonpagequestions = array();
310 $moveselectedonpage = optional_param('moveselectedonpagetop', 0, PARAM_INT);
311 if (!$moveselectedonpage) {
312 $moveselectedonpage = optional_param('moveselectedonpagebottom', 0, PARAM_INT);
315 foreach ($rawdata as $key => $value) {
316 if (preg_match('!^g([0-9]+)$!', $key, $matches)) {
317 // Parse input for question -> grades.
318 $questionid = $matches[1];
319 $quiz->grades[$questionid] = unformat_float($value);
320 quiz_update_question_instance($quiz->grades[$questionid], $questionid, $quiz);
321 $deletepreviews = true;
322 $recomputesummarks = true;
324 } else if (preg_match('!^o(pg)?([0-9]+)$!', $key, $matches)) {
325 // Parse input for ordering info.
326 $questionid = $matches[2];
327 // Make sure two questions don't overwrite each other. If we get a second
328 // question with the same position, shift the second one along to the next gap.
329 $value = clean_param($value, PARAM_INT);
330 while (array_key_exists($value, $questions)) {
331 $value++;
333 if ($matches[1]) {
334 // This is a page-break entry.
335 $questions[$value] = 0;
336 } else {
337 $questions[$value] = $questionid;
339 $deletepreviews = true;
343 // If ordering info was given, reorder the questions.
344 if ($questions) {
345 ksort($questions);
346 $questions[] = 0;
347 $quiz->questions = implode(',', $questions);
348 $DB->set_field('quiz', 'questions', $quiz->questions, array('id' => $quiz->id));
349 $deletepreviews = true;
352 // Get a list of questions to move, later to be added in the appropriate
353 // place in the string.
354 if ($moveselectedonpage) {
355 $questions = explode(',', $quiz->questions);
356 $newquestions = array();
357 // Remove the questions from their original positions first.
358 foreach ($questions as $questionid) {
359 if (!in_array($questionid, $selectedquestionids)) {
360 $newquestions[] = $questionid;
363 $questions = $newquestions;
365 // Move to the end of the selected page.
366 $pagebreakpositions = array_keys($questions, 0);
367 $numpages = count($pagebreakpositions);
369 // Ensure the target page number is in range.
370 for ($i = $moveselectedonpage; $i > $numpages; $i--) {
371 $questions[] = 0;
372 $pagebreakpositions[] = count($questions) - 1;
374 $moveselectedpos = $pagebreakpositions[$moveselectedonpage - 1];
376 // Do the move.
377 array_splice($questions, $moveselectedpos, 0, $selectedquestionids);
378 $quiz->questions = implode(',', $questions);
380 // Update the database.
381 $DB->set_field('quiz', 'questions', $quiz->questions, array('id' => $quiz->id));
382 $deletepreviews = true;
385 // If rescaling is required save the new maximum.
386 $maxgrade = unformat_float(optional_param('maxgrade', -1, PARAM_RAW));
387 if ($maxgrade >= 0) {
388 quiz_set_grade($maxgrade, $quiz);
391 if ($deletepreviews) {
392 quiz_delete_previews($quiz);
394 if ($recomputesummarks) {
395 quiz_update_sumgrades($quiz);
396 quiz_update_all_attempt_sumgrades($quiz);
397 quiz_update_all_final_grades($quiz);
398 quiz_update_grades($quiz, 0, true);
400 redirect($afteractionurl);
403 $questionbank->process_actions($thispageurl, $cm);
405 // End of process commands =====================================================
407 $PAGE->requires->skip_link_to('questionbank',
408 get_string('skipto', 'access', get_string('questionbank', 'question')));
409 $PAGE->requires->skip_link_to('quizcontentsblock',
410 get_string('skipto', 'access', get_string('questionsinthisquiz', 'quiz')));
411 $PAGE->set_title(get_string('editingquizx', 'quiz', format_string($quiz->name)));
412 $PAGE->set_heading($course->fullname);
413 $node = $PAGE->settingsnav->find('mod_quiz_edit', navigation_node::TYPE_SETTING);
414 if ($node) {
415 $node->make_active();
417 echo $OUTPUT->header();
419 // Initialise the JavaScript.
420 $quizeditconfig = new stdClass();
421 $quizeditconfig->url = $thispageurl->out(true, array('qbanktool' => '0'));
422 $quizeditconfig->dialoglisteners = array();
423 $numberoflisteners = max(quiz_number_of_pages($quiz->questions), 1);
424 for ($pageiter = 1; $pageiter <= $numberoflisteners; $pageiter++) {
425 $quizeditconfig->dialoglisteners[] = 'addrandomdialoglaunch_' . $pageiter;
427 $PAGE->requires->data_for_js('quiz_edit_config', $quizeditconfig);
428 $PAGE->requires->js('/question/qengine.js');
429 $module = array(
430 'name' => 'mod_quiz_edit',
431 'fullpath' => '/mod/quiz/edit.js',
432 'requires' => array('yui2-dom', 'yui2-event', 'yui2-container'),
433 'strings' => array(),
434 'async' => false,
436 $PAGE->requires->js_init_call('quiz_edit_init', null, false, $module);
438 // Print the tabs to switch mode.
439 if ($quiz_reordertool) {
440 $currenttab = 'reorder';
441 } else {
442 $currenttab = 'edit';
444 $tabs = array(array(
445 new tabobject('edit', new moodle_url($thispageurl,
446 array('reordertool' => 0)), get_string('editingquiz', 'quiz')),
447 new tabobject('reorder', new moodle_url($thispageurl,
448 array('reordertool' => 1)), get_string('orderingquiz', 'quiz')),
450 print_tabs($tabs, $currenttab);
452 if ($quiz_qbanktool) {
453 $bankclass = '';
454 $quizcontentsclass = '';
455 } else {
456 $bankclass = 'collapsed ';
457 $quizcontentsclass = 'quizwhenbankcollapsed';
460 echo '<div class="questionbankwindow ' . $bankclass . 'block">';
461 echo '<div class="header"><div class="title"><h2>';
462 echo get_string('questionbankcontents', 'quiz') .
463 '&nbsp;[<a href="' . $thispageurl->out(true, array('qbanktool' => '1')) .
464 '" id="showbankcmd">' . get_string('show').
465 '</a><a href="' . $thispageurl->out(true, array('qbanktool' => '0')) .
466 '" id="hidebankcmd">' . get_string('hide').
467 '</a>]';
468 echo '</h2></div></div><div class="content">';
470 echo '<span id="questionbank"></span>';
471 echo '<div class="container">';
472 echo '<div id="module" class="module">';
473 echo '<div class="bd">';
474 $questionbank->display('editq',
475 $pagevars['qpage'],
476 $pagevars['qperpage'],
477 $pagevars['cat'], $pagevars['recurse'], $pagevars['showhidden'],
478 $pagevars['qbshowtext']);
479 echo '</div>';
480 echo '</div>';
481 echo '</div>';
483 echo '</div></div>';
485 echo '<div class="quizcontents ' . $quizcontentsclass . '" id="quizcontentsblock">';
486 if ($quiz->shufflequestions) {
487 $repaginatingdisabledhtml = 'disabled="disabled"';
488 $repaginatingdisabled = true;
489 $quiz->questions = quiz_repaginate($quiz->questions, $quiz->questionsperpage);
490 } else {
491 $repaginatingdisabledhtml = '';
492 $repaginatingdisabled = false;
494 if ($quiz_reordertool) {
495 echo '<div class="repaginatecommand"><button id="repaginatecommand" ' .
496 $repaginatingdisabledhtml.'>'.
497 get_string('repaginatecommand', 'quiz').'...</button>';
498 echo '</div>';
501 if ($quiz_reordertool) {
502 echo $OUTPUT->heading_with_help(get_string('orderingquizx', 'quiz', format_string($quiz->name)),
503 'orderandpaging', 'quiz');
504 } else {
505 echo $OUTPUT->heading(get_string('editingquizx', 'quiz', format_string($quiz->name)), 2);
506 echo $OUTPUT->help_icon('editingquiz', 'quiz', get_string('basicideasofquiz', 'quiz'));
508 quiz_print_status_bar($quiz);
510 $tabindex = 0;
511 quiz_print_grading_form($quiz, $thispageurl, $tabindex);
513 $notifystrings = array();
514 if ($quizhasattempts) {
515 $reviewlink = quiz_attempt_summary_link_to_reports($quiz, $cm, $contexts->lowest());
516 $notifystrings[] = get_string('cannoteditafterattempts', 'quiz', $reviewlink);
518 if ($quiz->shufflequestions) {
519 $updateurl = new moodle_url("$CFG->wwwroot/course/mod.php",
520 array('return' => 'true', 'update' => $quiz->cmid, 'sesskey' => sesskey()));
521 $updatelink = '<a href="'.$updateurl->out().'">' . get_string('updatethis', '',
522 get_string('modulename', 'quiz')) . '</a>';
523 $notifystrings[] = get_string('shufflequestionsselected', 'quiz', $updatelink);
525 if (!empty($notifystrings)) {
526 echo $OUTPUT->box('<p>' . implode('</p><p>', $notifystrings) . '</p>', 'statusdisplay');
529 if ($quiz_reordertool) {
530 $perpage = array();
531 $perpage[0] = get_string('allinone', 'quiz');
532 for ($i = 1; $i <= 50; ++$i) {
533 $perpage[$i] = $i;
535 $gostring = get_string('go');
536 echo '<div id="repaginatedialog"><div class="hd">';
537 echo get_string('repaginatecommand', 'quiz');
538 echo '</div><div class="bd">';
539 echo '<form action="edit.php" method="post">';
540 echo '<fieldset class="invisiblefieldset">';
541 echo html_writer::input_hidden_params($thispageurl);
542 echo '<input type="hidden" name="sesskey" value="'.sesskey().'" />';
543 // YUI does not submit the value of the submit button so we need to add the value.
544 echo '<input type="hidden" name="repaginate" value="'.$gostring.'" />';
545 $attributes = array();
546 $attributes['disabled'] = $repaginatingdisabledhtml ? 'disabled' : null;
547 $select = html_writer::select(
548 $perpage, 'questionsperpage', $quiz->questionsperpage, null, $attributes);
549 print_string('repaginate', 'quiz', $select);
550 echo '<div class="quizquestionlistcontrols">';
551 echo ' <input type="submit" name="repaginate" value="'. $gostring . '" ' .
552 $repaginatingdisabledhtml.' />';
553 echo '</div></fieldset></form></div></div>';
556 if ($quiz_reordertool) {
557 echo '<div class="reorder">';
558 } else {
559 echo '<div class="editq">';
562 quiz_print_question_list($quiz, $thispageurl, true, $quiz_reordertool, $quiz_qbanktool,
563 $quizhasattempts, $defaultcategoryobj, $canaddquestion, $canaddrandom);
564 echo '</div>';
566 // Close <div class="quizcontents">.
567 echo '</div>';
569 if (!$quiz_reordertool && $canaddrandom) {
570 $randomform = new quiz_add_random_form(new moodle_url('/mod/quiz/addrandom.php'), $contexts);
571 $randomform->set_data(array(
572 'category' => $pagevars['cat'],
573 'returnurl' => $thispageurl->out_as_local_url(false),
574 'cmid' => $cm->id,
577 <div id="randomquestiondialog">
578 <div class="hd"><?php print_string('addrandomquestiontoquiz', 'quiz', $quiz->name); ?>
579 <span id="pagenumber"><!-- JavaScript will insert the page number here. -->
580 </span>
581 </div>
582 <div class="bd"><?php
583 $randomform->display();
584 ?></div>
585 </div>
586 <?php
588 echo $OUTPUT->footer();