1f108bf61384dfc8de2093a386cecb1b92a793aa
[moodle.git] / mod / quiz / edit.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/>.
18 /**
19  * Page to edit quizzes
20  *
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
27  *
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
37  *
38  * @package    mod
39  * @subpackage quiz
40  * @copyright  1999 onwards Martin Dougiamas and others {@link http://moodle.com}
41  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
42  */
45 require_once('../../config.php');
46 require_once($CFG->dirroot . '/mod/quiz/editlib.php');
47 require_once($CFG->dirroot . '/mod/quiz/addrandomform.php');
48 require_once($CFG->dirroot . '/question/category_class.php');
51 /**
52  * Callback function called from question_list() function
53  * (which is called from showbank())
54  * Displays button in form with checkboxes for each question.
55  */
56 function module_specific_buttons($cmid, $cmoptions) {
57     global $OUTPUT;
58     $params = array(
59         'type' => 'submit',
60         'name' => 'add',
61         'value' => $OUTPUT->larrow() . ' ' . get_string('addtoquiz', 'quiz'),
62     );
63     if ($cmoptions->hasattempts) {
64         $params['disabled'] = 'disabled';
65     }
66     return html_writer::empty_tag('input', $params);
67 }
69 /**
70  * Callback function called from question_list() function
71  * (which is called from showbank())
72  */
73 function module_specific_controls($totalnumber, $recurse, $category, $cmid, $cmoptions) {
74     global $OUTPUT;
75     $out = '';
76     $catcontext = get_context_instance_by_id($category->contextid);
77     if (has_capability('moodle/question:useall', $catcontext)) {
78         if ($cmoptions->hasattempts) {
79             $disabled = ' disabled="disabled"';
80         } else {
81             $disabled = '';
82         }
83         $randomusablequestions =
84                 question_bank::get_qtype('random')->get_available_questions_from_category(
85                         $category->id, $recurse);
86         $maxrand = count($randomusablequestions);
87         if ($maxrand > 0) {
88             for ($i = 1; $i <= min(10, $maxrand); $i++) {
89                 $randomcount[$i] = $i;
90             }
91             for ($i = 20; $i <= min(100, $maxrand); $i += 10) {
92                 $randomcount[$i] = $i;
93             }
94         } else {
95             $randomcount[0] = 0;
96             $disabled = ' disabled="disabled"';
97         }
99         $out = '<strong><label for="menurandomcount">'.get_string('addrandomfromcategory', 'quiz').
100                 '</label></strong><br />';
101         $attributes = array();
102         $attributes['disabled'] = $disabled ? 'disabled' : null;
103         $select = html_writer::select($randomcount, 'randomcount', '1', null, $attributes);
104         $out .= get_string('addrandom', 'quiz', $select);
105         $out .= '<input type="hidden" name="recurse" value="'.$recurse.'" />';
106         $out .= '<input type="hidden" name="categoryid" value="' . $category->id . '" />';
107         $out .= ' <input type="submit" name="addrandom" value="'.
108                 get_string('addtoquiz', 'quiz').'"' . $disabled . ' />';
109         $out .= $OUTPUT->help_icon('addarandomquestion', 'quiz');
110     }
111     return $out;
114 // These params are only passed from page request to request while we stay on
115 // this page otherwise they would go in question_edit_setup.
116 $quiz_reordertool = optional_param('reordertool', -1, PARAM_BOOL);
117 $quiz_qbanktool = optional_param('qbanktool', -1, PARAM_BOOL);
118 $scrollpos = optional_param('scrollpos', '', PARAM_INT);
120 list($thispageurl, $contexts, $cmid, $cm, $quiz, $pagevars) =
121         question_edit_setup('editq', '/mod/quiz/edit.php', true);
122 $quiz->questions = quiz_clean_layout($quiz->questions);
124 $defaultcategoryobj = question_make_default_categories($contexts->all());
125 $defaultcategory = $defaultcategoryobj->id . ',' . $defaultcategoryobj->contextid;
127 if ($quiz_qbanktool > -1) {
128     $thispageurl->param('qbanktool', $quiz_qbanktool);
129     set_user_preference('quiz_qbanktool_open', $quiz_qbanktool);
130 } else {
131     $quiz_qbanktool = get_user_preferences('quiz_qbanktool_open', 0);
134 if ($quiz_reordertool > -1) {
135     $thispageurl->param('reordertool', $quiz_reordertool);
136     set_user_preference('quiz_reordertab', $quiz_reordertool);
137 } else {
138     $quiz_reordertool = get_user_preferences('quiz_reordertab', 0);
141 $canaddrandom = $contexts->have_cap('moodle/question:useall');
142 $canaddquestion = (bool) $contexts->having_add_and_use();
144 $quizhasattempts = quiz_has_attempts($quiz->id);
146 $PAGE->set_url($thispageurl);
148 // Get the course object and related bits.
149 $course = $DB->get_record('course', array('id' => $quiz->course));
150 if (!$course) {
151     print_error('invalidcourseid', 'error');
154 $questionbank = new quiz_question_bank_view($contexts, $thispageurl, $course, $cm, $quiz);
155 $questionbank->set_quiz_has_attempts($quizhasattempts);
157 // Log this visit.
158 add_to_log($cm->course, 'quiz', 'editquestions',
159             "view.php?id=$cm->id", "$quiz->id", $cm->id);
161 // You need mod/quiz:manage in addition to question capabilities to access this page.
162 require_capability('mod/quiz:manage', $contexts->lowest());
164 if (empty($quiz->grades)) {
165     $quiz->grades = quiz_get_all_question_grades($quiz);
168 // Process commands ============================================================
169 if ($quiz->shufflequestions) {
170     // Strip page breaks before processing actions, so that re-ordering works
171     // as expected when shuffle questions is on.
172     $quiz->questions = quiz_repaginate($quiz->questions, 0);
175 // Get the list of question ids had their check-boxes ticked.
176 $selectedquestionids = array();
177 $params = (array) data_submitted();
178 foreach ($params as $key => $value) {
179     if (preg_match('!^s([0-9]+)$!', $key, $matches)) {
180         $selectedquestionids[] = $matches[1];
181     }
184 $afteractionurl = new moodle_url($thispageurl);
185 if ($scrollpos) {
186     $afteractionurl->param('scrollpos', $scrollpos);
188 if (($up = optional_param('up', false, PARAM_INT)) && confirm_sesskey()) {
189     $quiz->questions = quiz_move_question_up($quiz->questions, $up);
190     $DB->set_field('quiz', 'questions', $quiz->questions, array('id' => $quiz->id));
191     quiz_delete_previews($quiz);
192     redirect($afteractionurl);
195 if (($down = optional_param('down', false, PARAM_INT)) && confirm_sesskey()) {
196     $quiz->questions = quiz_move_question_down($quiz->questions, $down);
197     $DB->set_field('quiz', 'questions', $quiz->questions, array('id' => $quiz->id));
198     quiz_delete_previews($quiz);
199     redirect($afteractionurl);
202 if (optional_param('repaginate', false, PARAM_BOOL) && confirm_sesskey()) {
203     // Re-paginate the quiz.
204     $questionsperpage = optional_param('questionsperpage', $quiz->questionsperpage, PARAM_INT);
205     $quiz->questions = quiz_repaginate($quiz->questions, $questionsperpage );
206     $DB->set_field('quiz', 'questions', $quiz->questions, array('id' => $quiz->id));
207     quiz_delete_previews($quiz);
208     redirect($afteractionurl);
211 if (($addquestion = optional_param('addquestion', 0, PARAM_INT)) && confirm_sesskey()) {
212     // Add a single question to the current quiz.
213     quiz_require_question_use($addquestion);
214     $addonpage = optional_param('addonpage', 0, PARAM_INT);
215     quiz_add_quiz_question($addquestion, $quiz, $addonpage);
216     quiz_delete_previews($quiz);
217     quiz_update_sumgrades($quiz);
218     $thispageurl->param('lastchanged', $addquestion);
219     redirect($afteractionurl);
222 if (optional_param('add', false, PARAM_BOOL) && confirm_sesskey()) {
223     // Add selected questions to the current quiz.
224     $rawdata = (array) data_submitted();
225     foreach ($rawdata as $key => $value) { // Parse input for question ids.
226         if (preg_match('!^q([0-9]+)$!', $key, $matches)) {
227             $key = $matches[1];
228             quiz_require_question_use($key);
229             quiz_add_quiz_question($key, $quiz);
230         }
231     }
232     quiz_delete_previews($quiz);
233     quiz_update_sumgrades($quiz);
234     redirect($afteractionurl);
237 if ((optional_param('addrandom', false, PARAM_BOOL)) && confirm_sesskey()) {
238     // Add random questions to the quiz.
239     $recurse = optional_param('recurse', 0, PARAM_BOOL);
240     $addonpage = optional_param('addonpage', 0, PARAM_INT);
241     $categoryid = required_param('categoryid', PARAM_INT);
242     $randomcount = required_param('randomcount', PARAM_INT);
243     quiz_add_random_questions($quiz, $addonpage, $categoryid, $randomcount, $recurse);
245     quiz_delete_previews($quiz);
246     quiz_update_sumgrades($quiz);
247     redirect($afteractionurl);
250 if (optional_param('addnewpagesafterselected', null, PARAM_CLEAN) &&
251         !empty($selectedquestionids) && confirm_sesskey()) {
252     foreach ($selectedquestionids as $questionid) {
253         $quiz->questions = quiz_add_page_break_after($quiz->questions, $questionid);
254     }
255     $DB->set_field('quiz', 'questions', $quiz->questions, array('id' => $quiz->id));
256     quiz_delete_previews($quiz);
257     redirect($afteractionurl);
260 $addpage = optional_param('addpage', false, PARAM_INT);
261 if ($addpage !== false && confirm_sesskey()) {
262     $quiz->questions = quiz_add_page_break_at($quiz->questions, $addpage);
263     $DB->set_field('quiz', 'questions', $quiz->questions, array('id' => $quiz->id));
264     quiz_delete_previews($quiz);
265     redirect($afteractionurl);
268 $deleteemptypage = optional_param('deleteemptypage', false, PARAM_INT);
269 if (($deleteemptypage !== false) && confirm_sesskey()) {
270     $quiz->questions = quiz_delete_empty_page($quiz->questions, $deleteemptypage);
271     $DB->set_field('quiz', 'questions', $quiz->questions, array('id' => $quiz->id));
272     quiz_delete_previews($quiz);
273     redirect($afteractionurl);
276 $remove = optional_param('remove', false, PARAM_INT);
277 if ($remove && confirm_sesskey()) {
278     // Remove a question from the quiz.
279     // We require the user to have the 'use' capability on the question,
280     // so that then can add it back if they remove the wrong one by mistake.
281     quiz_require_question_use($remove);
282     quiz_remove_question($quiz, $remove);
283     quiz_delete_previews($quiz);
284     quiz_update_sumgrades($quiz);
285     redirect($afteractionurl);
288 if (optional_param('quizdeleteselected', false, PARAM_BOOL) &&
289         !empty($selectedquestionids) && confirm_sesskey()) {
290     foreach ($selectedquestionids as $questionid) {
291         if (quiz_has_question_use($questionid)) {
292             quiz_remove_question($quiz, $questionid);
293         }
294     }
295     quiz_delete_previews($quiz);
296     quiz_update_sumgrades($quiz);
297     redirect($afteractionurl);
300 if (optional_param('savechanges', false, PARAM_BOOL) && confirm_sesskey()) {
301     $deletepreviews = false;
302     $recomputesummarks = false;
304     $oldquestions = explode(',', $quiz->questions); // The questions in the old order.
305     $questions = array(); // For questions in the new order.
306     $rawdata = (array) data_submitted();
307     $moveonpagequestions = array();
308     $moveselectedonpage = optional_param('moveselectedonpagetop', 0, PARAM_INT);
309     if (!$moveselectedonpage) {
310         $moveselectedonpage = optional_param('moveselectedonpagebottom', 0, PARAM_INT);
311     }
313     foreach ($rawdata as $key => $value) {
314         if (preg_match('!^g([0-9]+)$!', $key, $matches)) {
315             // Parse input for question -> grades.
316             $questionid = $matches[1];
317             $quiz->grades[$questionid] = unformat_float($value);
318             quiz_update_question_instance($quiz->grades[$questionid], $questionid, $quiz);
319             $deletepreviews = true;
320             $recomputesummarks = true;
322         } else if (preg_match('!^o(pg)?([0-9]+)$!', $key, $matches)) {
323             // Parse input for ordering info.
324             $questionid = $matches[2];
325             // Make sure two questions don't overwrite each other. If we get a second
326             // question with the same position, shift the second one along to the next gap.
327             $value = clean_param($value, PARAM_INTEGER);
328             while (array_key_exists($value, $questions)) {
329                 $value++;
330             }
331             if ($matches[1]) {
332                 // This is a page-break entry.
333                 $questions[$value] = 0;
334             } else {
335                 $questions[$value] = $questionid;
336             }
337             $deletepreviews = true;
338         }
339     }
341     // If ordering info was given, reorder the questions.
342     if ($questions) {
343         ksort($questions);
344         $questions[] = 0;
345         $quiz->questions = implode(',', $questions);
346         $DB->set_field('quiz', 'questions', $quiz->questions, array('id' => $quiz->id));
347         $deletepreviews = true;
348     }
350     // Get a list of questions to move, later to be added in the appropriate
351     // place in the string.
352     if ($moveselectedonpage) {
353         $questions = explode(',', $quiz->questions);
354         $newquestions = array();
355         // Remove the questions from their original positions first.
356         foreach ($questions as $questionid) {
357             if (!in_array($questionid, $selectedquestionids)) {
358                 $newquestions[] = $questionid;
359             }
360         }
361         $questions = $newquestions;
363         // Move to the end of the selected page.
364         $pagebreakpositions = array_keys($questions, 0);
365         $numpages = count($pagebreakpositions);
367         // Ensure the target page number is in range.
368         for ($i = $moveselectedonpage; $i > $numpages; $i--) {
369             $questions[] = 0;
370             $pagebreakpositions[] = count($questions) - 1;
371         }
372         $moveselectedpos = $pagebreakpositions[$moveselectedonpage - 1];
374         // Do the move.
375         array_splice($questions, $moveselectedpos, 0, $selectedquestionids);
376         $quiz->questions = implode(',', $questions);
378         // Update the database.
379         $DB->set_field('quiz', 'questions', $quiz->questions, array('id' => $quiz->id));
380         $deletepreviews = true;
381     }
383     // If rescaling is required save the new maximum.
384     $maxgrade = unformat_float(optional_param('maxgrade', -1, PARAM_RAW));
385     if ($maxgrade >= 0) {
386         quiz_set_grade($maxgrade, $quiz);
387     }
389     if ($deletepreviews) {
390         quiz_delete_previews($quiz);
391     }
392     if ($recomputesummarks) {
393         quiz_update_sumgrades($quiz);
394         quiz_update_all_attempt_sumgrades($quiz);
395         quiz_update_all_final_grades($quiz);
396         quiz_update_grades($quiz, 0, true);
397     }
398     redirect($afteractionurl);
401 $questionbank->process_actions($thispageurl, $cm);
403 // End of process commands =====================================================
405 $PAGE->requires->yui2_lib('container');
406 $PAGE->requires->yui2_lib('dragdrop');
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 $PAGE->requires->js('/mod/quiz/edit.js');
430 $PAGE->requires->js_init_call('quiz_edit_init');
432 // Print the tabs to switch mode.
433 if ($quiz_reordertool) {
434     $currenttab = 'reorder';
435 } else {
436     $currenttab = 'edit';
438 $tabs = array(array(
439     new tabobject('edit', new moodle_url($thispageurl,
440             array('reordertool' => 0)), get_string('editingquiz', 'quiz')),
441     new tabobject('reorder', new moodle_url($thispageurl,
442             array('reordertool' => 1)), get_string('orderingquiz', 'quiz')),
443 ));
444 print_tabs($tabs, $currenttab);
446 if ($quiz_qbanktool) {
447     $bankclass = '';
448     $quizcontentsclass = '';
449 } else {
450     $bankclass = 'collapsed ';
451     $quizcontentsclass = 'quizwhenbankcollapsed';
454 echo '<div class="questionbankwindow ' . $bankclass . 'block">';
455 echo '<div class="header"><div class="title"><h2>';
456 echo get_string('questionbankcontents', 'quiz') .
457         ' <a href="' . $thispageurl->out(true, array('qbanktool' => '1')) .
458        '" id="showbankcmd">[' . get_string('show').
459        ']</a>
460        <a href="' . $thispageurl->out(true, array('qbanktool' => '0')) .
461        '" id="hidebankcmd">[' . get_string('hide').
462        ']</a>';
463 echo '</h2></div></div><div class="content">';
465 echo '<span id="questionbank"></span>';
466 echo '<div class="container">';
467 echo '<div id="module" class="module">';
468 echo '<div class="bd">';
469 $questionbank->display('editq',
470         $pagevars['qpage'],
471         $pagevars['qperpage'],
472         $pagevars['cat'], $pagevars['recurse'], $pagevars['showhidden'],
473         $pagevars['qbshowtext']);
474 echo '</div>';
475 echo '</div>';
476 echo '</div>';
478 echo '</div></div>';
480 echo '<div class="quizcontents ' . $quizcontentsclass . '" id="quizcontentsblock">';
481 if ($quiz->shufflequestions) {
482     $repaginatingdisabledhtml = 'disabled="disabled"';
483     $repaginatingdisabled = true;
484     $quiz->questions = quiz_repaginate($quiz->questions, $quiz->questionsperpage);
485 } else {
486     $repaginatingdisabledhtml = '';
487     $repaginatingdisabled = false;
489 if ($quiz_reordertool) {
490     echo '<div class="repaginatecommand"><button id="repaginatecommand" ' .
491             $repaginatingdisabledhtml.'>'.
492             get_string('repaginatecommand', 'quiz').'...</button>';
493     echo '</div>';
496 if ($quiz_reordertool) {
497     echo $OUTPUT->heading_with_help(get_string('orderingquizx', 'quiz', format_string($quiz->name)),
498             'orderandpaging', 'quiz');
499 } else {
500     echo $OUTPUT->heading(get_string('editingquizx', 'quiz', format_string($quiz->name)), 2);
501     echo $OUTPUT->help_icon('editingquiz', 'quiz', get_string('basicideasofquiz', 'quiz'));
503 quiz_print_status_bar($quiz);
505 $tabindex = 0;
506 quiz_print_grading_form($quiz, $thispageurl, $tabindex);
508 $notifystrings = array();
509 if ($quizhasattempts) {
510     $reviewlink = quiz_attempt_summary_link_to_reports($quiz, $cm, $contexts->lowest());
511     $notifystrings[] = get_string('cannoteditafterattempts', 'quiz', $reviewlink);
513 if ($quiz->shufflequestions) {
514     $updateurl = new moodle_url("$CFG->wwwroot/course/mod.php",
515             array('return' => 'true', 'update' => $quiz->cmid, 'sesskey' => sesskey()));
516     $updatelink = '<a href="'.$updateurl->out().'">' . get_string('updatethis', '',
517             get_string('modulename', 'quiz')) . '</a>';
518     $notifystrings[] = get_string('shufflequestionsselected', 'quiz', $updatelink);
520 if (!empty($notifystrings)) {
521     echo $OUTPUT->box('<p>' . implode('</p><p>', $notifystrings) . '</p>', 'statusdisplay');
524 if ($quiz_reordertool) {
525     $perpage = array();
526     $perpage[0] = get_string('allinone', 'quiz');
527     for ($i = 1; $i <= 50; ++$i) {
528         $perpage[$i] = $i;
529     }
530     $gostring = get_string('go');
531     echo '<div id="repaginatedialog"><div class="hd">';
532     echo get_string('repaginatecommand', 'quiz');
533     echo '</div><div class="bd">';
534     echo '<form action="edit.php" method="post">';
535     echo '<fieldset class="invisiblefieldset">';
536     echo html_writer::input_hidden_params($thispageurl);
537     echo '<input type="hidden" name="sesskey" value="'.sesskey().'" />';
538     // YUI does not submit the value of the submit button so we need to add the value.
539     echo '<input type="hidden" name="repaginate" value="'.$gostring.'" />';
540     $attributes = array();
541     $attributes['disabled'] = $repaginatingdisabledhtml ? 'disabled' : null;
542     $select = html_writer::select(
543             $perpage, 'questionsperpage', $quiz->questionsperpage, null, $attributes);
544     print_string('repaginate', 'quiz', $select);
545     echo '<div class="quizquestionlistcontrols">';
546     echo ' <input type="submit" name="repaginate" value="'. $gostring . '" ' .
547             $repaginatingdisabledhtml.' />';
548     echo '</div></fieldset></form></div></div>';
551 if ($quiz_reordertool) {
552     echo '<div class="reorder">';
553 } else {
554     echo '<div class="editq">';
557 quiz_print_question_list($quiz, $thispageurl, true, $quiz_reordertool, $quiz_qbanktool,
558         $quizhasattempts, $defaultcategoryobj, $canaddquestion, $canaddrandom);
559 echo '</div>';
561 // Close <div class="quizcontents">.
562 echo '</div>';
564 if (!$quiz_reordertool && $canaddrandom) {
565     $randomform = new quiz_add_random_form(new moodle_url('/mod/quiz/addrandom.php'), $contexts);
566     $randomform->set_data(array(
567         'category' => $pagevars['cat'],
568         'returnurl' => $thispageurl->out_as_local_url(false),
569         'cmid' => $cm->id,
570     ));
571     ?>
572     <div id="randomquestiondialog">
573     <div class="hd"><?php print_string('addrandomquestiontoquiz', 'quiz', $quiz->name); ?>
574     <span id="pagenumber"><!-- JavaScript will insert the page number here. -->
575     </span>
576     </div>
577     <div class="bd"><?php
578     $randomform->display();
579     ?></div>
580     </div>
581     <?php
583 echo $OUTPUT->footer();