MDL-33532 quiz editing: let uses enter locale floats.
[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 $pagetitle = get_string('editingquiz', 'quiz');
149 if ($quiz_reordertool) {
150     $pagetitle = get_string('orderingquiz', 'quiz');
152 // Get the course object and related bits.
153 $course = $DB->get_record('course', array('id' => $quiz->course));
154 if (!$course) {
155     print_error('invalidcourseid', 'error');
158 $questionbank = new quiz_question_bank_view($contexts, $thispageurl, $course, $cm, $quiz);
159 $questionbank->set_quiz_has_attempts($quizhasattempts);
161 // Log this visit.
162 add_to_log($cm->course, 'quiz', 'editquestions',
163             "view.php?id=$cm->id", "$quiz->id", $cm->id);
165 // You need mod/quiz:manage in addition to question capabilities to access this page.
166 require_capability('mod/quiz:manage', $contexts->lowest());
168 if (empty($quiz->grades)) {
169     $quiz->grades = quiz_get_all_question_grades($quiz);
172 // Process commands ============================================================
173 if ($quiz->shufflequestions) {
174     // Strip page breaks before processing actions, so that re-ordering works
175     // as expected when shuffle questions is on.
176     $quiz->questions = quiz_repaginate($quiz->questions, 0);
179 // Get the list of question ids had their check-boxes ticked.
180 $selectedquestionids = array();
181 $params = (array) data_submitted();
182 foreach ($params as $key => $value) {
183     if (preg_match('!^s([0-9]+)$!', $key, $matches)) {
184         $selectedquestionids[] = $matches[1];
185     }
188 $afteractionurl = new moodle_url($thispageurl);
189 if ($scrollpos) {
190     $afteractionurl->param('scrollpos', $scrollpos);
192 if (($up = optional_param('up', false, PARAM_INT)) && confirm_sesskey()) {
193     $quiz->questions = quiz_move_question_up($quiz->questions, $up);
194     $DB->set_field('quiz', 'questions', $quiz->questions, array('id' => $quiz->id));
195     quiz_delete_previews($quiz);
196     redirect($afteractionurl);
199 if (($down = optional_param('down', false, PARAM_INT)) && confirm_sesskey()) {
200     $quiz->questions = quiz_move_question_down($quiz->questions, $down);
201     $DB->set_field('quiz', 'questions', $quiz->questions, array('id' => $quiz->id));
202     quiz_delete_previews($quiz);
203     redirect($afteractionurl);
206 if (optional_param('repaginate', false, PARAM_BOOL) && confirm_sesskey()) {
207     // Re-paginate the quiz.
208     $questionsperpage = optional_param('questionsperpage', $quiz->questionsperpage, PARAM_INT);
209     $quiz->questions = quiz_repaginate($quiz->questions, $questionsperpage );
210     $DB->set_field('quiz', 'questions', $quiz->questions, array('id' => $quiz->id));
211     quiz_delete_previews($quiz);
212     redirect($afteractionurl);
215 if (($addquestion = optional_param('addquestion', 0, PARAM_INT)) && confirm_sesskey()) {
216     // Add a single question to the current quiz.
217     quiz_require_question_use($addquestion);
218     $addonpage = optional_param('addonpage', 0, PARAM_INT);
219     quiz_add_quiz_question($addquestion, $quiz, $addonpage);
220     quiz_delete_previews($quiz);
221     quiz_update_sumgrades($quiz);
222     $thispageurl->param('lastchanged', $addquestion);
223     redirect($afteractionurl);
226 if (optional_param('add', false, PARAM_BOOL) && confirm_sesskey()) {
227     // Add selected questions to the current quiz.
228     $rawdata = (array) data_submitted();
229     foreach ($rawdata as $key => $value) { // Parse input for question ids.
230         if (preg_match('!^q([0-9]+)$!', $key, $matches)) {
231             $key = $matches[1];
232             quiz_require_question_use($key);
233             quiz_add_quiz_question($key, $quiz);
234         }
235     }
236     quiz_delete_previews($quiz);
237     quiz_update_sumgrades($quiz);
238     redirect($afteractionurl);
241 if ((optional_param('addrandom', false, PARAM_BOOL)) && confirm_sesskey()) {
242     // Add random questions to the quiz.
243     $recurse = optional_param('recurse', 0, PARAM_BOOL);
244     $addonpage = optional_param('addonpage', 0, PARAM_INT);
245     $categoryid = required_param('categoryid', PARAM_INT);
246     $randomcount = required_param('randomcount', PARAM_INT);
247     quiz_add_random_questions($quiz, $addonpage, $categoryid, $randomcount, $recurse);
249     quiz_delete_previews($quiz);
250     quiz_update_sumgrades($quiz);
251     redirect($afteractionurl);
254 if (optional_param('addnewpagesafterselected', null, PARAM_CLEAN) &&
255         !empty($selectedquestionids) && confirm_sesskey()) {
256     foreach ($selectedquestionids as $questionid) {
257         $quiz->questions = quiz_add_page_break_after($quiz->questions, $questionid);
258     }
259     $DB->set_field('quiz', 'questions', $quiz->questions, array('id' => $quiz->id));
260     quiz_delete_previews($quiz);
261     redirect($afteractionurl);
264 $addpage = optional_param('addpage', false, PARAM_INT);
265 if ($addpage !== false && confirm_sesskey()) {
266     $quiz->questions = quiz_add_page_break_at($quiz->questions, $addpage);
267     $DB->set_field('quiz', 'questions', $quiz->questions, array('id' => $quiz->id));
268     quiz_delete_previews($quiz);
269     redirect($afteractionurl);
272 $deleteemptypage = optional_param('deleteemptypage', false, PARAM_INT);
273 if (($deleteemptypage !== false) && confirm_sesskey()) {
274     $quiz->questions = quiz_delete_empty_page($quiz->questions, $deleteemptypage);
275     $DB->set_field('quiz', 'questions', $quiz->questions, array('id' => $quiz->id));
276     quiz_delete_previews($quiz);
277     redirect($afteractionurl);
280 $remove = optional_param('remove', false, PARAM_INT);
281 if ($remove && confirm_sesskey()) {
282     // Remove a question from the quiz.
283     // We require the user to have the 'use' capability on the question,
284     // so that then can add it back if they remove the wrong one by mistake.
285     quiz_require_question_use($remove);
286     quiz_remove_question($quiz, $remove);
287     quiz_delete_previews($quiz);
288     quiz_update_sumgrades($quiz);
289     redirect($afteractionurl);
292 if (optional_param('quizdeleteselected', false, PARAM_BOOL) &&
293         !empty($selectedquestionids) && confirm_sesskey()) {
294     foreach ($selectedquestionids as $questionid) {
295         if (quiz_has_question_use($questionid)) {
296             quiz_remove_question($quiz, $questionid);
297         }
298     }
299     quiz_delete_previews($quiz);
300     quiz_update_sumgrades($quiz);
301     redirect($afteractionurl);
304 if (optional_param('savechanges', false, PARAM_BOOL) && confirm_sesskey()) {
305     $deletepreviews = false;
306     $recomputesummarks = false;
308     $oldquestions = explode(',', $quiz->questions); // The questions in the old order.
309     $questions = array(); // For questions in the new order.
310     $rawdata = (array) data_submitted();
311     $moveonpagequestions = array();
312     $moveselectedonpage = optional_param('moveselectedonpagetop', 0, PARAM_INT);
313     if (!$moveselectedonpage) {
314         $moveselectedonpage = optional_param('moveselectedonpagebottom', 0, PARAM_INT);
315     }
317     foreach ($rawdata as $key => $value) {
318         if (preg_match('!^g([0-9]+)$!', $key, $matches)) {
319             // Parse input for question -> grades.
320             $questionid = $matches[1];
321             $quiz->grades[$questionid] = unformat_float($value);
322             quiz_update_question_instance($quiz->grades[$questionid], $questionid, $quiz);
323             $deletepreviews = true;
324             $recomputesummarks = true;
326         } else if (preg_match('!^o(pg)?([0-9]+)$!', $key, $matches)) {
327             // Parse input for ordering info.
328             $questionid = $matches[2];
329             // Make sure two questions don't overwrite each other. If we get a second
330             // question with the same position, shift the second one along to the next gap.
331             $value = clean_param($value, PARAM_INTEGER);
332             while (array_key_exists($value, $questions)) {
333                 $value++;
334             }
335             if ($matches[1]) {
336                 // This is a page-break entry.
337                 $questions[$value] = 0;
338             } else {
339                 $questions[$value] = $questionid;
340             }
341             $deletepreviews = true;
342         }
343     }
345     // If ordering info was given, reorder the questions.
346     if ($questions) {
347         ksort($questions);
348         $questions[] = 0;
349         $quiz->questions = implode(',', $questions);
350         $DB->set_field('quiz', 'questions', $quiz->questions, array('id' => $quiz->id));
351         $deletepreviews = true;
352     }
354     // Get a list of questions to move, later to be added in the appropriate
355     // place in the string.
356     if ($moveselectedonpage) {
357         $questions = explode(',', $quiz->questions);
358         $newquestions = array();
359         // Remove the questions from their original positions first.
360         foreach ($questions as $questionid) {
361             if (!in_array($questionid, $selectedquestionids)) {
362                 $newquestions[] = $questionid;
363             }
364         }
365         $questions = $newquestions;
367         // Move to the end of the selected page.
368         $pagebreakpositions = array_keys($questions, 0);
369         $numpages = count($pagebreakpositions);
371         // Ensure the target page number is in range.
372         for ($i = $moveselectedonpage; $i > $numpages; $i--) {
373             $questions[] = 0;
374             $pagebreakpositions[] = count($questions) - 1;
375         }
376         $moveselectedpos = $pagebreakpositions[$moveselectedonpage - 1];
378         // Do the move.
379         array_splice($questions, $moveselectedpos, 0, $selectedquestionids);
380         $quiz->questions = implode(',', $questions);
382         // Update the database.
383         $DB->set_field('quiz', 'questions', $quiz->questions, array('id' => $quiz->id));
384         $deletepreviews = true;
385     }
387     // If rescaling is required save the new maximum.
388     $maxgrade = unformat_float(optional_param('maxgrade', -1, PARAM_RAW));
389     if ($maxgrade >= 0) {
390         quiz_set_grade($maxgrade, $quiz);
391     }
393     if ($deletepreviews) {
394         quiz_delete_previews($quiz);
395     }
396     if ($recomputesummarks) {
397         quiz_update_sumgrades($quiz);
398         quiz_update_all_attempt_sumgrades($quiz);
399         quiz_update_all_final_grades($quiz);
400         quiz_update_grades($quiz, 0, true);
401     }
402     redirect($afteractionurl);
405 $questionbank->process_actions($thispageurl, $cm);
407 // End of process commands =====================================================
409 $PAGE->requires->yui2_lib('container');
410 $PAGE->requires->yui2_lib('dragdrop');
411 $PAGE->requires->skip_link_to('questionbank',
412         get_string('skipto', 'access', get_string('questionbank', 'question')));
413 $PAGE->requires->skip_link_to('quizcontentsblock',
414         get_string('skipto', 'access', get_string('questionsinthisquiz', 'quiz')));
415 $PAGE->set_title($pagetitle);
416 $PAGE->set_heading($course->fullname);
417 $node = $PAGE->settingsnav->find('mod_quiz_edit', navigation_node::TYPE_SETTING);
418 if ($node) {
419     $node->make_active();
421 echo $OUTPUT->header();
423 // Initialise the JavaScript.
424 $quizeditconfig = new stdClass();
425 $quizeditconfig->url = $thispageurl->out(true, array('qbanktool' => '0'));
426 $quizeditconfig->dialoglisteners = array();
427 $numberoflisteners = max(quiz_number_of_pages($quiz->questions), 1);
428 for ($pageiter = 1; $pageiter <= $numberoflisteners; $pageiter++) {
429     $quizeditconfig->dialoglisteners[] = 'addrandomdialoglaunch_' . $pageiter;
431 $PAGE->requires->data_for_js('quiz_edit_config', $quizeditconfig);
432 $PAGE->requires->js('/question/qengine.js');
433 $PAGE->requires->js('/mod/quiz/edit.js');
434 $PAGE->requires->js_init_call('quiz_edit_init');
436 // Print the tabs to switch mode.
437 if ($quiz_reordertool) {
438     $currenttab = 'reorder';
439 } else {
440     $currenttab = 'edit';
442 $tabs = array(array(
443     new tabobject('edit', new moodle_url($thispageurl,
444             array('reordertool' => 0)), get_string('editingquiz', 'quiz')),
445     new tabobject('reorder', new moodle_url($thispageurl,
446             array('reordertool' => 1)), get_string('orderingquiz', 'quiz')),
447 ));
448 print_tabs($tabs, $currenttab);
450 if ($quiz_qbanktool) {
451     $bankclass = '';
452     $quizcontentsclass = '';
453 } else {
454     $bankclass = 'collapsed ';
455     $quizcontentsclass = 'quizwhenbankcollapsed';
458 echo '<div class="questionbankwindow ' . $bankclass . 'block">';
459 echo '<div class="header"><div class="title"><h2>';
460 echo get_string('questionbankcontents', 'quiz') .
461         ' <a href="' . $thispageurl->out(true, array('qbanktool' => '1')) .
462        '" id="showbankcmd">[' . get_string('show').
463        ']</a>
464        <a href="' . $thispageurl->out(true, array('qbanktool' => '0')) .
465        '" id="hidebankcmd">[' . get_string('hide').
466        ']</a>';
467 echo '</h2></div></div><div class="content">';
469 echo '<span id="questionbank"></span>';
470 echo '<div class="container">';
471 echo '<div id="module" class="module">';
472 echo '<div class="bd">';
473 $questionbank->display('editq',
474         $pagevars['qpage'],
475         $pagevars['qperpage'],
476         $pagevars['cat'], $pagevars['recurse'], $pagevars['showhidden'],
477         $pagevars['qbshowtext']);
478 echo '</div>';
479 echo '</div>';
480 echo '</div>';
482 echo '</div></div>';
484 echo '<div class="quizcontents ' . $quizcontentsclass . '" id="quizcontentsblock">';
485 if ($quiz->shufflequestions) {
486     $repaginatingdisabledhtml = 'disabled="disabled"';
487     $repaginatingdisabled = true;
488     $quiz->questions = quiz_repaginate($quiz->questions, $quiz->questionsperpage);
489 } else {
490     $repaginatingdisabledhtml = '';
491     $repaginatingdisabled = false;
493 if ($quiz_reordertool) {
494     echo '<div class="repaginatecommand"><button id="repaginatecommand" ' .
495             $repaginatingdisabledhtml.'>'.
496             get_string('repaginatecommand', 'quiz').'...</button>';
497     echo '</div>';
500 if ($quiz_reordertool) {
501     echo $OUTPUT->heading_with_help(get_string('orderingquiz', 'quiz') . ': ' . $quiz->name,
502             'orderandpaging', 'quiz');
503 } else {
504     echo $OUTPUT->heading(get_string('editingquiz', 'quiz') . ': ' . $quiz->name, 2);
505     echo $OUTPUT->help_icon('editingquiz', 'quiz', get_string('basicideasofquiz', 'quiz'));
507 quiz_print_status_bar($quiz);
509 $tabindex = 0;
510 quiz_print_grading_form($quiz, $thispageurl, $tabindex);
512 $notifystrings = array();
513 if ($quizhasattempts) {
514     $reviewlink = quiz_attempt_summary_link_to_reports($quiz, $cm, $contexts->lowest());
515     $notifystrings[] = get_string('cannoteditafterattempts', 'quiz', $reviewlink);
517 if ($quiz->shufflequestions) {
518     $updateurl = new moodle_url("$CFG->wwwroot/course/mod.php",
519             array('return' => 'true', 'update' => $quiz->cmid, 'sesskey' => sesskey()));
520     $updatelink = '<a href="'.$updateurl->out().'">' . get_string('updatethis', '',
521             get_string('modulename', 'quiz')) . '</a>';
522     $notifystrings[] = get_string('shufflequestionsselected', 'quiz', $updatelink);
524 if (!empty($notifystrings)) {
525     echo $OUTPUT->box('<p>' . implode('</p><p>', $notifystrings) . '</p>', 'statusdisplay');
528 if ($quiz_reordertool) {
529     $perpage = array();
530     $perpage[0] = get_string('allinone', 'quiz');
531     for ($i = 1; $i <= 50; ++$i) {
532         $perpage[$i] = $i;
533     }
534     $gostring = get_string('go');
535     echo '<div id="repaginatedialog"><div class="hd">';
536     echo get_string('repaginatecommand', 'quiz');
537     echo '</div><div class="bd">';
538     echo '<form action="edit.php" method="post">';
539     echo '<fieldset class="invisiblefieldset">';
540     echo html_writer::input_hidden_params($thispageurl);
541     echo '<input type="hidden" name="sesskey" value="'.sesskey().'" />';
542     // YUI does not submit the value of the submit button so we need to add the value.
543     echo '<input type="hidden" name="repaginate" value="'.$gostring.'" />';
544     $attributes = array();
545     $attributes['disabled'] = $repaginatingdisabledhtml ? 'disabled' : null;
546     $select = html_writer::select(
547             $perpage, 'questionsperpage', $quiz->questionsperpage, null, $attributes);
548     print_string('repaginate', 'quiz', $select);
549     echo '<div class="quizquestionlistcontrols">';
550     echo ' <input type="submit" name="repaginate" value="'. $gostring . '" ' .
551             $repaginatingdisabledhtml.' />';
552     echo '</div></fieldset></form></div></div>';
555 if ($quiz_reordertool) {
556     echo '<div class="reorder">';
557 } else {
558     echo '<div class="editq">';
561 quiz_print_question_list($quiz, $thispageurl, true, $quiz_reordertool, $quiz_qbanktool,
562         $quizhasattempts, $defaultcategoryobj, $canaddquestion, $canaddrandom);
563 echo '</div>';
565 // Close <div class="quizcontents">.
566 echo '</div>';
568 if (!$quiz_reordertool && $canaddrandom) {
569     $randomform = new quiz_add_random_form(new moodle_url('/mod/quiz/addrandom.php'), $contexts);
570     $randomform->set_data(array(
571         'category' => $pagevars['cat'],
572         'returnurl' => $thispageurl->out_as_local_url(false),
573         'cmid' => $cm->id,
574     ));
575     ?>
576     <div id="randomquestiondialog">
577     <div class="hd"><?php print_string('addrandomquestiontoquiz', 'quiz', $quiz->name); ?>
578     <span id="pagenumber"><!-- JavaScript will insert the page number here. -->
579     </span>
580     </div>
581     <div class="bd"><?php
582     $randomform->display();
583     ?></div>
584     </div>
585     <?php
587 echo $OUTPUT->footer();