9f653c0b2351d1c746cc1a813d29ffc729b29a0e
[moodle.git] / mod / quiz / editlib.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  * This contains functions that are called from within the quiz module only
20  * Functions that are also called by core Moodle are in {@link lib.php}
21  * This script also loads the code in {@link questionlib.php} which holds
22  * the module-indpendent code for handling questions and which in turn
23  * initialises all the questiontype classes.
24  *
25  * @package    mod
26  * @subpackage quiz
27  * @copyright  1999 onwards Martin Dougiamas and others {@link http://moodle.com}
28  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
29  */
32 defined('MOODLE_INTERNAL') || die();
34 require_once($CFG->dirroot . '/mod/quiz/locallib.php');
36 define('NUM_QS_TO_SHOW_IN_RANDOM', 3);
38 /**
39  * Remove a question from a quiz
40  * @param object $quiz the quiz object.
41  * @param int $questionid The id of the question to be deleted.
42  */
43 function quiz_remove_question($quiz, $questionid) {
44     global $DB;
46     $questionids = explode(',', $quiz->questions);
47     $key = array_search($questionid, $questionids);
48     if ($key === false) {
49         return;
50     }
52     unset($questionids[$key]);
53     $quiz->questions = implode(',', $questionids);
54     $DB->set_field('quiz', 'questions', $quiz->questions, array('id' => $quiz->id));
55     $DB->delete_records('quiz_question_instances',
56             array('quiz' => $quiz->instance, 'question' => $questionid));
57 }
59 /**
60  * Remove an empty page from the quiz layout. If that is not possible, do nothing.
61  * @param string $layout the existinng layout, $quiz->questions.
62  * @param int $index the position into $layout where the empty page should be removed.
63  * @return the updated layout
64  */
65 function quiz_delete_empty_page($layout, $index) {
66     $questionids = explode(',', $layout);
68     if ($index < -1 || $index >= count($questionids) - 1) {
69         return $layout;
70     }
72     if (($index >= 0 && $questionids[$index] != 0) || $questionids[$index + 1] != 0) {
73         return $layout; // This was not an empty page.
74     }
76     unset($questionids[$index + 1]);
78     return implode(',', $questionids);
79 }
81 /**
82  * Add a question to a quiz
83  *
84  * Adds a question to a quiz by updating $quiz as well as the
85  * quiz and quiz_question_instances tables. It also adds a page break
86  * if required.
87  * @param int $id The id of the question to be added
88  * @param object $quiz The extended quiz object as used by edit.php
89  *      This is updated by this function
90  * @param int $page Which page in quiz to add the question on. If 0 (default),
91  *      add at the end
92  * @return bool false if the question was already in the quiz
93  */
94 function quiz_add_quiz_question($id, $quiz, $page = 0) {
95     global $DB;
96     $questions = explode(',', quiz_clean_layout($quiz->questions));
97     if (in_array($id, $questions)) {
98         return false;
99     }
101     // remove ending page break if it is not needed
102     if ($breaks = array_keys($questions, 0)) {
103         // determine location of the last two page breaks
104         $end = end($breaks);
105         $last = prev($breaks);
106         $last = $last ? $last : -1;
107         if (!$quiz->questionsperpage || (($end - $last - 1) < $quiz->questionsperpage)) {
108             array_pop($questions);
109         }
110     }
111     if (is_int($page) && $page >= 1) {
112         $numofpages = quiz_number_of_pages($quiz->questions);
113         if ($numofpages<$page) {
114             //the page specified does not exist in quiz
115             $page = 0;
116         } else {
117             // add ending page break - the following logic requires doing
118             //this at this point
119             $questions[] = 0;
120             $currentpage = 1;
121             $addnow = false;
122             foreach ($questions as $question) {
123                 if ($question == 0) {
124                     $currentpage++;
125                     //The current page is the one after the one we want to add on,
126                     //so we add the question before adding the current page.
127                     if ($currentpage == $page + 1) {
128                         $questions_new[] = $id;
129                     }
130                 }
131                 $questions_new[] = $question;
132             }
133             $questions = $questions_new;
134         }
135     }
136     if ($page == 0) {
137         // add question
138         $questions[] = $id;
139         // add ending page break
140         $questions[] = 0;
141     }
143     // Save new questionslist in database
144     $quiz->questions = implode(',', $questions);
145     $DB->set_field('quiz', 'questions', $quiz->questions, array('id' => $quiz->id));
147     // Add the new question instance.
148     $instance = new stdClass();
149     $instance->quiz = $quiz->id;
150     $instance->question = $id;
151     $instance->grade = $DB->get_field('question', 'defaultmark', array('id' => $id));
152     $DB->insert_record('quiz_question_instances', $instance);
155 function quiz_add_random_questions($quiz, $addonpage, $categoryid, $number,
156         $includesubcategories) {
157     global $DB;
159     $category = $DB->get_record('question_categories', array('id' => $categoryid));
160     if (!$category) {
161         print_error('invalidcategoryid', 'error');
162     }
164     $catcontext = get_context_instance_by_id($category->contextid);
165     require_capability('moodle/question:useall', $catcontext);
167     // Find existing random questions in this category that are
168     // not used by any quiz.
169     if ($existingquestions = $DB->get_records_sql(
170             "SELECT q.id, q.qtype FROM {question} q
171             WHERE qtype = 'random'
172                 AND category = ?
173                 AND " . $DB->sql_compare_text('questiontext') . " = ?
174                 AND NOT EXISTS (
175                         SELECT *
176                           FROM {quiz_question_instances}
177                          WHERE question = q.id)
178             ORDER BY id", array($category->id, $includesubcategories))) {
179         // Take as many of these as needed.
180         while (($existingquestion = array_shift($existingquestions)) && $number > 0) {
181             quiz_add_quiz_question($existingquestion->id, $quiz, $addonpage);
182             $number -= 1;
183         }
184     }
186     if ($number <= 0) {
187         return;
188     }
190     // More random questions are needed, create them.
191     for ($i = 0; $i < $number; $i += 1) {
192         $form = new stdClass();
193         $form->questiontext = array('text' => $includesubcategories, 'format' => 0);
194         $form->category = $category->id . ',' . $category->contextid;
195         $form->defaultmark = 1;
196         $form->hidden = 1;
197         $form->stamp = make_unique_id_code(); // Set the unique code (not to be changed)
198         $question = new stdClass();
199         $question->qtype = 'random';
200         $question = question_bank::get_qtype('random')->save_question($question, $form);
201         if (!isset($question->id)) {
202             print_error('cannotinsertrandomquestion', 'quiz');
203         }
204         quiz_add_quiz_question($question->id, $quiz, $addonpage);
205     }
208 /**
209  * Add a page break after at particular position$.
210  * @param string $layout the existinng layout, $quiz->questions.
211  * @param int $index the position into $layout where the empty page should be removed.
212  * @return the updated layout
213  */
214 function quiz_add_page_break_at($layout, $index) {
215     $questionids = explode(',', $layout);
216     if ($index < 0 || $index >= count($questionids)) {
217         return $layout;
218     }
220     array_splice($questionids, $index, 0, '0');
222     return implode(',', $questionids);
225 /**
226  * Add a page break after a particular question.
227  * @param string $layout the existinng layout, $quiz->questions.
228  * @param int $qustionid the question to add the page break after.
229  * @return the updated layout
230  */
231 function quiz_add_page_break_after($layout, $questionid) {
232     $questionids = explode(',', $layout);
233     $key = array_search($questionid, $questionids);
234     if ($key === false || !$questionid) {
235         return $layout;
236     }
238     array_splice($questionids, $key + 1, 0, '0');
240     return implode(',', $questionids);
243 /**
244  * Update the database after $quiz->questions has been changed. For example,
245  * this deletes preview attempts and updates $quiz->sumgrades.
246  * @param $quiz the quiz object.
247  */
248 function quiz_save_new_layout($quiz) {
249     global $DB;
250     $DB->set_field('quiz', 'questions', $quiz->questions, array('id' => $quiz->id));
251     quiz_update_sumgrades($quiz);
252     quiz_delete_previews($quiz);
255 /**
256  * Save changes to question instance
257  *
258  * Saves changes to the question grades in the quiz_question_instances table.
259  * It does not update 'sumgrades' in the quiz table.
260  *
261  * @param int grade    The maximal grade for the question
262  * @param int $questionid  The id of the question
263  * @param int $quizid  The id of the quiz to update / add the instances for.
264  */
265 function quiz_update_question_instance($grade, $questionid, $quiz) {
266     global $DB;
267     $instance = $DB->get_record('quiz_question_instances', array('quiz' => $quiz->id,
268             'question' => $questionid));
269     $slot = quiz_get_slot_for_question($quiz, $questionid);
271     if (!$instance || !$slot) {
272         throw new coding_exception('Attempt to change the grade of a quesion not in the quiz.');
273     }
275     if (abs($grade - $instance->grade) < 1e-7) {
276         // Grade has not changed. Nothing to do.
277         return;
278     }
280     $instance->grade = $grade;
281     $DB->update_record('quiz_question_instances', $instance);
282     question_engine::set_max_mark_in_attempts(new qubaids_for_quiz($quiz->id),
283             $slot, $grade);
286 // Private function used by the following two.
287 function _quiz_move_question($layout, $questionid, $shift) {
288     if (!$questionid || !($shift == 1 || $shift == -1)) {
289         return $layout;
290     }
292     $questionids = explode(',', $layout);
293     $key = array_search($questionid, $questionids);
294     if ($key === false) {
295         return $layout;
296     }
298     $otherkey = $key + $shift;
299     if ($otherkey < 0 || $otherkey >= count($questionids) - 1) {
300         return $layout;
301     }
303     $temp = $questionids[$otherkey];
304     $questionids[$otherkey] = $questionids[$key];
305     $questionids[$key] = $temp;
307     return implode(',', $questionids);
310 /**
311  * Move a particular question one space earlier in the $quiz->questions list.
312  * If that is not possible, do nothing.
313  * @param string $layout the existinng layout, $quiz->questions.
314  * @param int $questionid the id of a question.
315  * @return the updated layout
316  */
317 function quiz_move_question_up($layout, $questionid) {
318     return _quiz_move_question($layout, $questionid, -1);
321 /**
322  * Move a particular question one space later in the $quiz->questions list.
323  * If that is not possible, do nothing.
324  * @param string $layout the existinng layout, $quiz->questions.
325  * @param int $questionid the id of a question.
326  * @return the updated layout
327  */
328 function quiz_move_question_down($layout, $questionid) {
329     return _quiz_move_question($layout, $questionid, +1);
332 /**
333  * Prints a list of quiz questions for the edit.php main view for edit
334  * ($reordertool = false) and order and paging ($reordertool = true) tabs
335  *
336  * @return int sum of maximum grades
337  * @param object $quiz This is not the standard quiz object used elsewhere but
338  *     it contains the quiz layout in $quiz->questions and the grades in
339  *     $quiz->grades
340  * @param object $pageurl The url of the current page with the parameters required
341  *     for links returning to the current page, as a moodle_url object
342  * @param bool $allowdelete Indicates whether the delete icons should be displayed
343  * @param bool $reordertool  Indicates whether the reorder tool should be displayed
344  * @param bool $quiz_qbanktool  Indicates whether the question bank should be displayed
345  * @param bool $hasattempts  Indicates whether the quiz has attempts
346  */
347 function quiz_print_question_list($quiz, $pageurl, $allowdelete, $reordertool,
348         $quiz_qbanktool, $hasattempts, $defaultcategoryobj) {
349     global $USER, $CFG, $DB, $OUTPUT;
350     $strorder = get_string('order');
351     $strquestionname = get_string('questionname', 'quiz');
352     $strgrade = get_string('grade');
353     $strremove = get_string('remove', 'quiz');
354     $stredit = get_string('edit');
355     $strview = get_string('view');
356     $straction = get_string('action');
357     $strmove = get_string('move');
358     $strmoveup = get_string('moveup');
359     $strmovedown = get_string('movedown');
360     $strsave = get_string('save', 'quiz');
361     $strreorderquestions = get_string('reorderquestions', 'quiz');
363     $strselectall = get_string('selectall', 'quiz');
364     $strselectnone = get_string('selectnone', 'quiz');
365     $strtype = get_string('type', 'quiz');
366     $strpreview = get_string('preview', 'quiz');
368     if ($quiz->questions) {
369         list($usql, $params) = $DB->get_in_or_equal(explode(',', $quiz->questions));
370         $params[] = $quiz->id;
371         $questions = $DB->get_records_sql("SELECT q.*, qc.contextid, qqi.grade as maxmark
372                               FROM {question} q
373                               JOIN {question_categories} qc ON qc.id = q.category
374                               JOIN {quiz_question_instances} qqi ON qqi.question = q.id
375                              WHERE q.id $usql AND qqi.quiz = ?", $params);
376     } else {
377         $questions = array();
378     }
380     $layout = quiz_clean_layout($quiz->questions);
381     $order = explode(',', $layout);
382     $lastindex = count($order) - 1;
384     $disabled = '';
385     $pagingdisabled = '';
386     if ($hasattempts) {
387         $disabled = 'disabled="disabled"';
388     }
389     if ($hasattempts || $quiz->shufflequestions) {
390         $pagingdisabled = 'disabled="disabled"';
391     }
393     $reordercontrolssetdefaultsubmit = '<div style="display:none;">' .
394         '<input type="submit" name="savechanges" value="' .
395         $strreorderquestions . '" ' . $pagingdisabled . ' /></div>';
396     $reordercontrols1 = '<div class="addnewpagesafterselected">' .
397         '<input type="submit" name="addnewpagesafterselected" value="' .
398         get_string('addnewpagesafterselected', 'quiz') . '"  ' .
399         $pagingdisabled . ' /></div>';
400     $reordercontrols1 .= '<div class="quizdeleteselected">' .
401         '<input type="submit" name="quizdeleteselected" ' .
402         'onclick="return confirm(\'' .
403         get_string('areyousureremoveselected', 'quiz') . '\');" value="' .
404         get_string('removeselected', 'quiz') . '"  ' . $disabled . ' /></div>';
406     $a = '<input name="moveselectedonpagetop" type="text" size="2" ' .
407         $pagingdisabled . ' />';
409     $reordercontrols2top = '<div class="moveselectedonpage">' .
410         get_string('moveselectedonpage', 'quiz', $a) .
411         '<input type="submit" name="savechanges" value="' .
412         $strmove . '"  ' . $pagingdisabled . ' />' . '
413         <br /><input type="submit" name="savechanges" value="' .
414         $strreorderquestions . '" /></div>';
415     $reordercontrols2bottom = '<div class="moveselectedonpage">' .
416         '<input type="submit" name="savechanges" value="' .
417         $strreorderquestions . '" /><br />' .
418         get_string('moveselectedonpage', 'quiz', $a) .
419         '<input type="submit" name="savechanges" value="' .
420         $strmove . '"  ' . $pagingdisabled . ' /> ' . '</div>';
422     $reordercontrols3 = '<a href="javascript:select_all_in(\'FORM\', null, ' .
423             '\'quizquestions\');">' .
424             $strselectall . '</a> /';
425     $reordercontrols3.=    ' <a href="javascript:deselect_all_in(\'FORM\', ' .
426             'null, \'quizquestions\');">' .
427             $strselectnone . '</a>';
429     $reordercontrolstop = '<div class="reordercontrols">' .
430             $reordercontrolssetdefaultsubmit .
431             $reordercontrols1 . $reordercontrols2top . $reordercontrols3 . "</div>";
432     $reordercontrolsbottom = '<div class="reordercontrols">' .
433             $reordercontrolssetdefaultsubmit .
434             $reordercontrols2bottom . $reordercontrols1 . $reordercontrols3 . "</div>";
436     if ($reordertool) {
437         echo '<form method="post" action="edit.php" id="quizquestions"><div>';
439         echo html_writer::input_hidden_params($pageurl);
440         echo '<input type="hidden" name="sesskey" value="' . sesskey() . '" />';
442         echo $reordercontrolstop;
443     }
445     //the current question ordinal (no descriptions)
446     $qno = 1;
447     //the current question (includes questions and descriptions)
448     $questioncount = 0;
449     //the current page number in iteration
450     $pagecount = 0;
452     $pageopen = false;
454     $returnurl = str_replace($CFG->wwwroot, '', $pageurl->out(false));
455     $questiontotalcount = count($order);
457     foreach ($order as $count => $qnum) {
459         $reordercheckbox = '';
460         $reordercheckboxlabel = '';
461         $reordercheckboxlabelclose = '';
463         if ($qnum && empty($questions[$qnum])) {
464             continue;
465         }
467         // If the questiontype is missing change the question type
468         if ($qnum && !array_key_exists($qnum, $questions)) {
469             $fakequestion = new stdClass();
470             $fakequestion->id = 0;
471             $fakequestion->qtype = 'missingtype';
472             $fakequestion->name = get_string('deletedquestion', 'qtype_missingtype');
473             $fakequestion->questiontext = '<p>' .
474                     get_string('deletedquestion', 'qtype_missing') . '</p>';
475             $fakequestion->length = 0;
476             $questions[$qnum] = $fakequestion;
477             $quiz->grades[$qnum] = 0;
479         } else if ($qnum && !question_bank::qtype_exists($questions[$qnum]->qtype)) {
480             $questions[$qnum]->qtype = 'missingtype';
481         }
483         if ($qnum != 0 || ($qnum == 0 && !$pageopen)) {
484             //this is either a question or a page break after another
485             //        (no page is currently open)
486             if (!$pageopen) {
487                 //if no page is open, start display of a page
488                 $pagecount++;
489                 echo  '<div class="quizpage"><span class="pagetitle">' .
490                         get_string('page') . '&nbsp;' . $pagecount .
491                         '</span><div class="pagecontent">';
492                 $pageopen = true;
493             }
494             if ($qnum == 0  && $count < $questiontotalcount) {
495                 // This is the second successive page break. Tell the user the page is empty.
496                 echo '<div class="pagestatus">';
497                 print_string('noquestionsonpage', 'quiz');
498                 echo '</div>';
499                 if ($allowdelete) {
500                     echo '<div class="quizpagedelete">';
501                     echo $OUTPUT->action_icon($pageurl->out(true,
502                             array('deleteemptypage' => $count - 1, 'sesskey'=>sesskey())),
503                             new pix_icon('t/delete', $strremove),
504                             new component_action('click',
505                                     'M.core_scroll_manager.save_scroll_action'),
506                             array('title' => $strremove));
507                     echo '</div>';
508                 }
509             }
511             if ($qnum != 0) {
512                 $question = $questions[$qnum];
513                 $questionparams = array(
514                         'returnurl' => $returnurl,
515                         'cmid' => $quiz->cmid,
516                         'id' => $question->id);
517                 $questionurl = new moodle_url('/question/question.php',
518                         $questionparams);
519                 $questioncount++;
520                 //this is an actual question
522                 /* Display question start */
523                 ?>
524 <div class="question">
525     <div class="questioncontainer <?php echo $question->qtype; ?>">
526         <div class="qnum">
527                 <?php
528                 $reordercheckbox = '';
529                 $reordercheckboxlabel = '';
530                 $reordercheckboxlabelclose = '';
531                 if ($reordertool) {
532                     $reordercheckbox = '<input type="checkbox" name="s' . $question->id .
533                         '" id="s' . $question->id . '" />';
534                     $reordercheckboxlabel = '<label for="s' . $question->id . '">';
535                     $reordercheckboxlabelclose = '</label>';
536                 }
537                 if ($question->length == 0) {
538                     $qnodisplay = get_string('infoshort', 'quiz');
539                 } else if ($quiz->shufflequestions) {
540                     $qnodisplay = '?';
541                 } else {
542                     if ($qno > 999 || ($reordertool && $qno > 99)) {
543                         $qnodisplay = html_writer::tag('small', $qno);
544                     } else {
545                         $qnodisplay = $qno;
546                     }
547                     $qno += $question->length;
548                 }
549                 echo $reordercheckboxlabel . $qnodisplay . $reordercheckboxlabelclose .
550                         $reordercheckbox;
552                 ?>
553         </div>
554         <div class="content">
555             <div class="questioncontrols">
556                 <?php
557                 if ($count != 0) {
558                     if (!$hasattempts) {
559                         $upbuttonclass = '';
560                         if ($count >= $lastindex - 1) {
561                             $upbuttonclass = 'upwithoutdown';
562                         }
563                         echo $OUTPUT->action_icon($pageurl->out(true,
564                                 array('up' => $question->id, 'sesskey'=>sesskey())),
565                                 new pix_icon('t/up', $strmoveup),
566                                 new component_action('click',
567                                         'M.core_scroll_manager.save_scroll_action'),
568                                 array('title' => $strmoveup));
569                     }
571                 }
572                 if ($count < $lastindex - 1) {
573                     if (!$hasattempts) {
574                         echo $OUTPUT->action_icon($pageurl->out(true,
575                                 array('down' => $question->id, 'sesskey'=>sesskey())),
576                                 new pix_icon('t/down', $strmovedown),
577                                 new component_action('click',
578                                         'M.core_scroll_manager.save_scroll_action'),
579                                 array('title' => $strmovedown));
580                     }
581                 }
582                 if ($allowdelete && (empty($question->id) ||
583                         question_has_capability_on($question, 'use', $question->category))) {
584                     // remove from quiz, not question delete.
585                     if (!$hasattempts) {
586                         echo $OUTPUT->action_icon($pageurl->out(true,
587                                 array('remove' => $question->id, 'sesskey'=>sesskey())),
588                                 new pix_icon('t/delete', $strremove),
589                                 new component_action('click',
590                                         'M.core_scroll_manager.save_scroll_action'),
591                                 array('title' => $strremove));
592                     }
593                 }
594                 ?>
595             </div><?php
596                 if ($question->qtype != 'description' && !$reordertool) {
597                     ?>
598 <div class="points">
599 <form method="post" action="edit.php" class="quizsavegradesform"><div>
600     <fieldset class="invisiblefieldset" style="display: block;">
601     <label for="<?php echo "inputq$question->id" ?>"><?php echo $strgrade; ?></label>:<br />
602     <input type="hidden" name="sesskey" value="<?php echo sesskey() ?>" />
603     <?php echo html_writer::input_hidden_params($pageurl); ?>
604     <input type="hidden" name="savechanges" value="save" />
605                     <?php
606                     echo '<input type="text" name="g' . $question->id .
607                             '" id="inputq' . $question->id .
608                             '" size="' . ($quiz->decimalpoints + 2) .
609                             '" value="' . (0 + $quiz->grades[$qnum]) .
610                             '" tabindex="' . ($lastindex + $qno) . '" />';
611                     ?>
612         <input type="submit" class="pointssubmitbutton" value="<?php echo $strsave; ?>" />
613     </fieldset>
614                     <?php
615                     if ($question->qtype == 'random') {
616                         echo '<a href="' . $questionurl->out() .
617                                 '" class="configurerandomquestion">' .
618                                 get_string("configurerandomquestion", "quiz") . '</a>';
619                     }
621                     ?>
622 </div>
623 </form>
625             </div>
626                     <?php
627                 } else if ($reordertool) {
628                     if ($qnum) {
629                         ?>
630 <div class="qorder">
631                         <?php
632                         echo '<input type="text" name="o' . $question->id .
633                                 '" size="2" value="' . (10*$count + 10) .
634                                 '" tabindex="' . ($lastindex + $qno) . '" />';
635                         ?>
636 </div>
637                         <?php
638                     }
639                 }
640                 ?>
641             <div class="questioncontentcontainer">
642                 <?php
643                 if ($question->qtype == 'random') { // it is a random question
644                     if (!$reordertool) {
645                         quiz_print_randomquestion($question, $pageurl, $quiz, $quiz_qbanktool);
646                     } else {
647                         quiz_print_randomquestion_reordertool($question, $pageurl, $quiz);
648                     }
649                 } else { // it is a single question
650                     if (!$reordertool) {
651                         quiz_print_singlequestion($question, $returnurl, $quiz);
652                     } else {
653                         quiz_print_singlequestion_reordertool($question, $returnurl, $quiz);
654                     }
655                 }
656                 ?>
657             </div>
658         </div>
659     </div>
660 </div>
662                 <?php
663             }
664         }
665         //a page break: end the existing page.
666         if ($qnum == 0) {
667             if ($pageopen) {
668                 if (!$reordertool && !($quiz->shufflequestions &&
669                         $count < $questiontotalcount - 1)) {
670                     quiz_print_pagecontrols($quiz, $pageurl, $pagecount,
671                             $hasattempts, $defaultcategoryobj);
672                 } else if ($count < $questiontotalcount - 1) {
673                     //do not include the last page break for reordering
674                     //to avoid creating a new extra page in the end
675                     echo '<input type="hidden" name="opg' . $pagecount . '" size="2" value="' .
676                             (10*$count + 10) . '" />';
677                 }
678                 echo "</div></div>";
680                 if (!$reordertool && !$quiz->shufflequestions) {
681                     echo $OUTPUT->container_start('addpage');
682                     $url = new moodle_url($pageurl->out_omit_querystring(),
683                             array('cmid' => $quiz->cmid, 'courseid' => $quiz->course,
684                                     'addpage' => $count, 'sesskey' => sesskey()));
685                     echo $OUTPUT->single_button($url, get_string('addpagehere', 'quiz'), 'post',
686                             array('disabled' => $hasattempts,
687                             'actions' => array(new component_action('click',
688                                     'M.core_scroll_manager.save_scroll_action'))));
689                     echo $OUTPUT->container_end();
690                 }
691                 $pageopen = false;
692                 $count++;
693             }
694         }
696     }
697     if ($reordertool) {
698         echo $reordercontrolsbottom;
699         echo '</div></form>';
700     }
703 /**
704  * Print all the controls for adding questions directly into the
705  * specific page in the edit tab of edit.php
706  *
707  * @param unknown_type $quiz
708  * @param unknown_type $pageurl
709  * @param unknown_type $page
710  * @param unknown_type $hasattempts
711  */
712 function quiz_print_pagecontrols($quiz, $pageurl, $page, $hasattempts, $defaultcategoryobj) {
713     global $CFG, $OUTPUT;
714     static $randombuttoncount = 0;
715     $randombuttoncount++;
716     echo '<div class="pagecontrols">';
718     // Get the current context
719     $thiscontext = get_context_instance(CONTEXT_COURSE, $quiz->course);
720     $contexts = new question_edit_contexts($thiscontext);
722     // Get the default category.
723     list($defaultcategoryid) = explode(',', $pageurl->param('cat'));
724     if (empty($defaultcategoryid)) {
725         $defaultcategoryid = $defaultcategoryobj->id;
726     }
728     // Create the url the question page will return to
729     $returnurladdtoquiz = new moodle_url($pageurl, array('addonpage' => $page));
731     // Print a button linking to the choose question type page.
732     $returnurladdtoquiz = str_replace($CFG->wwwroot, '', $returnurladdtoquiz->out(false));
733     $newquestionparams = array('returnurl' => $returnurladdtoquiz,
734             'cmid' => $quiz->cmid, 'appendqnumstring' => 'addquestion');
735     create_new_question_button($defaultcategoryid, $newquestionparams,
736             get_string('addaquestion', 'quiz'),
737             get_string('createquestionandadd', 'quiz'), $hasattempts);
739     if ($hasattempts) {
740         $disabled = 'disabled="disabled"';
741     } else {
742         $disabled = '';
743     }
744     ?>
745     <div class="singlebutton">
746         <form class="randomquestionform" action="<?php echo $CFG->wwwroot;
747                 ?>/mod/quiz/addrandom.php" method="get">
748             <div>
749                 <input type="hidden" class="addonpage_formelement" name="addonpage" value="<?php
750                         echo $page; ?>" />
751                 <input type="hidden" name="cmid" value="<?php echo $quiz->cmid; ?>" />
752                 <input type="hidden" name="courseid" value="<?php echo $quiz->course; ?>" />
753                 <input type="hidden" name="category" value="<?php
754                         echo $pageurl->param('cat'); ?>" />
755                 <input type="hidden" name="returnurl" value="<?php
756                         echo s(str_replace($CFG->wwwroot, '', $pageurl->out(false))); ?>" />
757                 <input type="submit" id="addrandomdialoglaunch_<?php
758                         echo $randombuttoncount; ?>" value="<?php
759                         echo get_string('addarandomquestion', 'quiz'); ?>" <?php
760                         echo " $disabled"; ?> />
761             </div>
762         </form>
763     </div>
764     <?php echo $OUTPUT->help_icon('addarandomquestion', 'quiz'); ?>
765     <?php
766     echo "\n</div>";
769 /**
770  * Print a given single question in quiz for the edit tab of edit.php.
771  * Meant to be used from quiz_print_question_list()
772  *
773  * @param object $question A question object from the database questions table
774  * @param object $returnurl The url to get back to this page, for example after editing.
775  * @param object $quiz The quiz in the context of which the question is being displayed
776  */
777 function quiz_print_singlequestion($question, $returnurl, $quiz) {
778     echo '<div class="singlequestion">';
779     echo quiz_question_edit_button($quiz->cmid, $question, $returnurl,
780             quiz_question_tostring($question) . ' ');
781     echo '<span class="questiontype">';
782     print_question_icon($question);
783     echo ' ' . question_bank::get_qtype_name($question->qtype) . '</span>';
784     echo '<span class="questionpreview">' .
785             quiz_question_preview_button($quiz, $question, true) . '</span>';
786     echo "</div>\n";
788 /**
789  * Print a given random question in quiz for the edit tab of edit.php.
790  * Meant to be used from quiz_print_question_list()
791  *
792  * @param object $question A question object from the database questions table
793  * @param object $questionurl The url of the question editing page as a moodle_url object
794  * @param object $quiz The quiz in the context of which the question is being displayed
795  * @param bool $quiz_qbanktool Indicate to this function if the question bank window open
796  */
797 function quiz_print_randomquestion(&$question, &$pageurl, &$quiz, $quiz_qbanktool) {
798     global $DB, $OUTPUT;
799     echo '<div class="quiz_randomquestion">';
801     if (!$category = $DB->get_record('question_categories',
802             array('id' => $question->category))) {
803         echo $OUTPUT->notification('Random question category not found!');
804         return;
805     }
807     echo '<div class="randomquestionfromcategory">';
808     print_question_icon($question);
809     print_random_option_icon($question);
810     echo ' ' . get_string('randomfromcategory', 'quiz') . '</div>';
812     $a = new stdClass();
813     $a->arrow = $OUTPUT->rarrow();
814     $strshowcategorycontents = get_string('showcategorycontents', 'quiz', $a);
816     $openqbankurl = $pageurl->out(true, array('qbanktool' => 1,
817             'cat' => $category->id . ',' . $category->contextid));
818     $linkcategorycontents = ' <a href="' . $openqbankurl . '">' . $strshowcategorycontents . '</a>';
820     echo '<div class="randomquestioncategory">';
821     echo '<a href="' . $openqbankurl . '" title="' . $strshowcategorycontents . '">' .
822             $category->name . '</a>';
823     echo '<span class="questionpreview">' .
824             quiz_question_preview_button($quiz, $question, true) . '</span>';
825     echo '</div>';
827     $questionids = question_bank::get_qtype('random')->get_available_questions_from_category(
828             $category->id, $question->questiontext == '1', '0');
829     $questioncount = count($questionids);
831     echo '<div class="randomquestionqlist">';
832     if ($questioncount == 0) {
833         // No questions in category, give an error plus instructions
834         echo '<span class="error">';
835         print_string('noquestionsnotinuse', 'quiz');
836         echo '</span>';
837         echo '<br />';
839         // Embed the link into the string with instructions
840         $a = new stdClass();
841         $a->catname = '<strong>' . $category->name . '</strong>';
842         $a->link = $linkcategorycontents;
843         echo get_string('addnewquestionsqbank', 'quiz', $a);
845     } else {
846         // Category has questions
848         // Get a sample from the database,
849         $questionidstoshow = array_slice($questionids, 0, NUM_QS_TO_SHOW_IN_RANDOM);
850         $questionstoshow = $DB->get_records_list('question', 'id', $questionidstoshow,
851                 '', 'id, qtype, name, questiontext, questiontextformat');
853         // list them,
854         echo '<ul>';
855         foreach ($questionstoshow as $question) {
856             echo '<li>' . quiz_question_tostring($question, true) . '</li>';
857         }
859         // and then display the total number.
860         echo '<li class="totalquestionsinrandomqcategory">';
861         if ($questioncount > NUM_QS_TO_SHOW_IN_RANDOM) {
862             echo '... ';
863         }
864         print_string('totalquestionsinrandomqcategory', 'quiz', $questioncount);
865         echo ' ' . $linkcategorycontents;
866         echo '</li>';
867         echo '</ul>';
868     }
870     echo '</div>';
871     echo '<div class="randomquestioncategorycount">';
872     echo '</div>';
873     echo '</div>';
876 /**
877  * Print a given single question in quiz for the reordertool tab of edit.php.
878  * Meant to be used from quiz_print_question_list()
879  *
880  * @param object $question A question object from the database questions table
881  * @param object $questionurl The url of the question editing page as a moodle_url object
882  * @param object $quiz The quiz in the context of which the question is being displayed
883  */
884 function quiz_print_singlequestion_reordertool($question, $returnurl, $quiz) {
885     echo '<div class="singlequestion">';
886     echo '<label for="s' . $question->id . '">';
887     print_question_icon($question);
888     echo ' ' . quiz_question_tostring($question);
889     echo '</label>';
890     echo '<span class="questionpreview">' .
891             quiz_question_action_icons($quiz, $quiz->cmid, $question, $returnurl) . '</span>';
892     echo "</div>\n";
895 /**
896  * Print a given random question in quiz for the reordertool tab of edit.php.
897  * Meant to be used from quiz_print_question_list()
898  *
899  * @param object $question A question object from the database questions table
900  * @param object $questionurl The url of the question editing page as a moodle_url object
901  * @param object $quiz The quiz in the context of which the question is being displayed
902  */
903 function quiz_print_randomquestion_reordertool(&$question, &$pageurl, &$quiz) {
904     global $DB, $OUTPUT;
906     // Load the category, and the number of available questions in it.
907     if (!$category = $DB->get_record('question_categories', array('id' => $question->category))) {
908         echo $OUTPUT->notification('Random question category not found!');
909         return;
910     }
911     $questioncount = count(question_bank::get_qtype(
912             'random')->get_available_questions_from_category(
913             $category->id, $question->questiontext == '1', '0'));
915     $reordercheckboxlabel = '<label for="s' . $question->id . '">';
916     $reordercheckboxlabelclose = '</label>';
918     echo '<div class="quiz_randomquestion">';
919     echo '<div class="randomquestionfromcategory">';
920     echo $reordercheckboxlabel;
921     print_question_icon($question);
922     print_random_option_icon($question);
924     if ($questioncount == 0) {
925         echo '<span class="error">';
926         print_string('empty', 'quiz');
927         echo '</span> ';
928     }
930     print_string('random', 'quiz');
931     echo ": $reordercheckboxlabelclose</div>";
933     echo '<div class="randomquestioncategory">';
934     echo $reordercheckboxlabel . $category->name . $reordercheckboxlabelclose;
935     echo '<span class="questionpreview">';
936     echo quiz_question_preview_button($quiz, $question, false);
937     echo '</span>';
938     echo "</div>";
940     echo '<div class="randomquestioncategorycount">';
941     echo '</div>';
942     echo '</div>';
945 /**
946  * Print an icon to indicate the 'include subcategories' state of a random question.
947  * @param $question the random question.
948  */
949 function print_random_option_icon($question) {
950     global $OUTPUT;
951     if (!empty($question->questiontext)) {
952         $icon = 'withsubcat';
953         $tooltip = get_string('randomwithsubcat', 'quiz');
954     } else {
955         $icon = 'nosubcat';
956         $tooltip = get_string('randomnosubcat', 'quiz');
957     }
958     echo '<img src="' . $OUTPUT->pix_url('i/' . $icon) . '" alt="' .
959             $tooltip . '" title="' . $tooltip . '" class="uihint" />';
962 /**
963  * Creates a textual representation of a question for display.
964  *
965  * @param object $question A question object from the database questions table
966  * @param bool $showicon If true, show the question's icon with the question. False by default.
967  * @param bool $showquestiontext If true (default), show question text after question name.
968  *       If false, show only question name.
969  * @param bool $return If true (default), return the output. If false, print it.
970  */
971 function quiz_question_tostring($question, $showicon = false,
972         $showquestiontext = true, $return = true) {
973     global $COURSE;
974     $result = '';
975     $result .= '<span class="questionname">';
976     if ($showicon) {
977         $result .= print_question_icon($question, true);
978         echo ' ';
979     }
980     $result .= shorten_text(format_string($question->name), 200) . '</span>';
981     if ($showquestiontext) {
982         $formatoptions = new stdClass();
983         $formatoptions->noclean = true;
984         $formatoptions->para = false;
985         $questiontext = strip_tags(format_text($question->questiontext,
986                 $question->questiontextformat,
987                 $formatoptions, $COURSE->id));
988         $questiontext = shorten_text($questiontext, 200);
989         $result .= '<span class="questiontext">';
990         if (!empty($questiontext)) {
991             $result .= $questiontext;
992         } else {
993             $result .= '<span class="error">';
994             $result .= get_string('questiontextisempty', 'quiz');
995             $result .= '</span>';
996         }
997         $result .= '</span>';
998     }
999     if ($return) {
1000         return $result;
1001     } else {
1002         echo $result;
1003     }
1006 /**
1007  * A column type for the add this question to the quiz.
1008  *
1009  * @copyright  2009 Tim Hunt
1010  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1011  */
1012 class question_bank_add_to_quiz_action_column extends question_bank_action_column_base {
1013     protected $stradd;
1015     public function init() {
1016         parent::init();
1017         $this->stradd = get_string('addtoquiz', 'quiz');
1018     }
1020     public function get_name() {
1021         return 'addtoquizaction';
1022     }
1024     protected function display_content($question, $rowclasses) {
1025         // for RTL languages: switch right and left arrows
1026         if (right_to_left()) {
1027             $movearrow = 't/removeright';
1028         } else {
1029             $movearrow = 't/moveleft';
1030         }
1031         $this->print_icon($movearrow, $this->stradd, $this->qbank->add_to_quiz_url($question->id));
1032     }
1034     public function get_required_fields() {
1035         return array('q.id');
1036     }
1039 /**
1040  * A column type for the name followed by the start of the question text.
1041  *
1042  * @copyright  2009 Tim Hunt
1043  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1044  */
1045 class question_bank_question_name_text_column extends question_bank_question_name_column {
1046     public function get_name() {
1047         return 'questionnametext';
1048     }
1050     protected function display_content($question, $rowclasses) {
1051         echo '<div>';
1052         $labelfor = $this->label_for($question);
1053         if ($labelfor) {
1054             echo '<label for="' . $labelfor . '">';
1055         }
1056         echo quiz_question_tostring($question, false, true, true);
1057         if ($labelfor) {
1058             echo '</label>';
1059         }
1060         echo '</div>';
1061     }
1063     public function get_required_fields() {
1064         $fields = parent::get_required_fields();
1065         $fields[] = 'q.questiontext';
1066         $fields[] = 'q.questiontextformat';
1067         return $fields;
1068     }
1071 /**
1072  * Subclass to customise the view of the question bank for the quiz editing screen.
1073  *
1074  * @copyright  2009 Tim Hunt
1075  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1076  */
1077 class quiz_question_bank_view extends question_bank_view {
1078     protected $quizhasattempts = false;
1079     /** @var object the quiz settings. */
1080     protected $quiz = false;
1082     /**
1083      * Constructor
1084      * @param question_edit_contexts $contexts
1085      * @param moodle_url $pageurl
1086      * @param object $course course settings
1087      * @param object $cm activity settings.
1088      * @param object $quiz quiz settings.
1089      */
1090     public function __construct($contexts, $pageurl, $course, $cm, $quiz) {
1091         parent::__construct($contexts, $pageurl, $course, $cm);
1092         $this->quiz = $quiz;
1093     }
1095     protected function known_field_types() {
1096         $types = parent::known_field_types();
1097         $types[] = new question_bank_add_to_quiz_action_column($this);
1098         $types[] = new question_bank_question_name_text_column($this);
1099         return $types;
1100     }
1102     protected function wanted_columns() {
1103         return array('addtoquizaction', 'checkbox', 'qtype', 'questionnametext',
1104                 'editaction', 'previewaction');
1105     }
1107     /**
1108      * Let the question bank display know whether the quiz has been attempted,
1109      * hence whether some bits of UI, like the add this question to the quiz icon,
1110      * should be displayed.
1111      * @param bool $quizhasattempts whether the quiz has attempts.
1112      */
1113     public function set_quiz_has_attempts($quizhasattempts) {
1114         $this->quizhasattempts = $quizhasattempts;
1115         if ($quizhasattempts && isset($this->visiblecolumns['addtoquizaction'])) {
1116             unset($this->visiblecolumns['addtoquizaction']);
1117         }
1118     }
1120     public function preview_question_url($question) {
1121         return quiz_question_preview_url($this->quiz, $question);
1122     }
1124     public function add_to_quiz_url($questionid) {
1125         global $CFG;
1126         $params = $this->baseurl->params();
1127         $params['addquestion'] = $questionid;
1128         $params['sesskey'] = sesskey();
1129         return new moodle_url('/mod/quiz/edit.php', $params);
1130     }
1132     public function display($tabname, $page, $perpage, $cat,
1133             $recurse, $showhidden, $showquestiontext) {
1134         global $OUTPUT;
1135         if ($this->process_actions_needing_ui()) {
1136             return;
1137         }
1139         // Display the current category.
1140         if (!$category = $this->get_current_category($cat)) {
1141             return;
1142         }
1143         $this->print_category_info($category);
1145         echo $OUTPUT->box_start('generalbox questionbank');
1147         $this->display_category_form($this->contexts->having_one_edit_tab_cap($tabname),
1148                 $this->baseurl, $cat);
1150         // continues with list of questions
1151         $this->display_question_list($this->contexts->having_one_edit_tab_cap($tabname),
1152                 $this->baseurl, $cat, $this->cm, $recurse, $page,
1153                 $perpage, $showhidden, $showquestiontext,
1154                 $this->contexts->having_cap('moodle/question:add'));
1156         $this->display_options($recurse, $showhidden, $showquestiontext);
1157         echo $OUTPUT->box_end();
1158     }
1160     protected function print_choose_category_message($categoryandcontext) {
1161         global $OUTPUT;
1162         echo $OUTPUT->box_start('generalbox questionbank');
1163         $this->display_category_form($this->contexts->having_one_edit_tab_cap('edit'),
1164                 $this->baseurl, $categoryandcontext);
1165         echo "<p style=\"text-align:center;\"><b>";
1166         print_string('selectcategoryabove', 'quiz');
1167         echo "</b></p>";
1168         echo $OUTPUT->box_end();
1169     }
1171     protected function print_category_info($category) {
1172         $formatoptions = new stdClass();
1173         $formatoptions->noclean = true;
1174         $strcategory = get_string('category', 'quiz');
1175         echo '<div class="categoryinfo"><div class="categorynamefieldcontainer">' .
1176                 $strcategory;
1177         echo ': <span class="categorynamefield">';
1178         echo shorten_text(strip_tags(format_string($category->name)), 60);
1179         echo '</span></div><div class="categoryinfofieldcontainer">' .
1180                 '<span class="categoryinfofield">';
1181         echo shorten_text(strip_tags(format_text($category->info, $category->infoformat,
1182                 $formatoptions, $this->course->id)), 200);
1183         echo '</span></div></div>';
1184     }
1186     protected function display_options($recurse, $showhidden, $showquestiontext) {
1187         echo '<form method="get" action="edit.php" id="displayoptions">';
1188         echo "<fieldset class='invisiblefieldset'>";
1189         echo html_writer::input_hidden_params($this->baseurl,
1190                 array('recurse', 'showhidden', 'showquestiontext'));
1191         $this->display_category_form_checkbox('recurse', $recurse,
1192                 get_string('includesubcategories', 'question'));
1193         $this->display_category_form_checkbox('showhidden', $showhidden,
1194                 get_string('showhidden', 'question'));
1195         echo '<noscript><div class="centerpara"><input type="submit" value="' .
1196                 get_string('go') . '" />';
1197         echo '</div></noscript></fieldset></form>';
1198     }
1201 /**
1202  * Prints the form for setting a quiz' overall grade
1203  *
1204  * @param object $quiz The quiz object of the quiz in question
1205  * @param object $pageurl The url of the current page with the parameters required
1206  *     for links returning to the current page, as a moodle_url object
1207  * @param int $tabindex The tabindex to start from for the form elements created
1208  * @return int The tabindex from which the calling page can continue, that is,
1209  *      the last value used +1.
1210  */
1211 function quiz_print_grading_form($quiz, $pageurl, $tabindex) {
1212     global $USER, $OUTPUT;
1213     $strsave = get_string('save', 'quiz');
1214     echo '<form method="post" action="edit.php" class="quizsavegradesform"><div>';
1215     echo '<fieldset class="invisiblefieldset" style="display: block;">';
1216     echo "<input type=\"hidden\" name=\"sesskey\" value=\"" . sesskey() . "\" />";
1217     echo html_writer::input_hidden_params($pageurl);
1218     $a = '<input type="text" id="inputmaxgrade" name="maxgrade" size="' .
1219             ($quiz->decimalpoints + 2) . '" tabindex="' . $tabindex
1220          . '" value="' . quiz_format_grade($quiz, $quiz->grade) . '" />';
1221     echo '<label for="inputmaxgrade">' . get_string('maximumgradex', '', $a) . "</label>";
1222     echo '<input type="hidden" name="savechanges" value="save" />';
1223     echo '<input type="submit" value="' . $strsave . '" />';
1224     echo '</fieldset>';
1225     echo "</div></form>\n";
1226     return $tabindex + 1;
1229 /**
1230  * Print the status bar
1231  *
1232  * @param object $quiz The quiz object of the quiz in question
1233  */
1234 function quiz_print_status_bar($quiz) {
1235     global $CFG;
1237     $bits = array();
1239     $bits[] = html_writer::tag('span',
1240             get_string('totalpointsx', 'quiz', quiz_format_grade($quiz, $quiz->sumgrades)),
1241             array('class' => 'totalpoints'));
1243     $bits[] = html_writer::tag('span',
1244             get_string('numquestionsx', 'quiz', quiz_number_of_questions_in_quiz($quiz->questions)),
1245             array('class' => 'numberofquestions'));
1247     $timenow = time();
1249     // Exact open and close dates for the tool-tip.
1250     $dates = array();
1251     if ($quiz->timeopen > 0) {
1252         if ($timenow > $quiz->timeopen) {
1253             $dates[] = get_string('quizopenedon', 'quiz', userdate($quiz->timeopen));
1254         } else {
1255             $dates[] = get_string('quizwillopen', 'quiz', userdate($quiz->timeopen));
1256         }
1257     }
1258     if ($quiz->timeclose > 0) {
1259         if ($timenow > $quiz->timeclose) {
1260             $dates[] = get_string('quizclosed', 'quiz', userdate($quiz->timeclose));
1261         } else {
1262             $dates[] = get_string('quizcloseson', 'quiz', userdate($quiz->timeclose));
1263         }
1264     }
1265     if (empty($dates)) {
1266         $dates[] = get_string('alwaysavailable', 'quiz');
1267     }
1268     $tooltip = implode(', ', $dates);;
1270     // Brief summary on the page.
1271     if ($timenow < $quiz->timeopen) {
1272         $currentstatus = get_string('quizisclosedwillopen', 'quiz',
1273                 userdate($quiz->timeclose, get_string('strftimedatetimeshort', 'langconfig')));
1274     } else if ($quiz->timeclose && $timenow <= $quiz->timeclose) {
1275         $currentstatus = get_string('quizisopenwillclose', 'quiz',
1276                 userdate($quiz->timeclose, get_string('strftimedatetimeshort', 'langconfig')));
1277     } else if ($quiz->timeclose && $timenow > $quiz->timeclose) {
1278         $currentstatus = get_string('quizisclosed', 'quiz');
1279     } else {
1280         $currentstatus = get_string('quizisopen', 'quiz');
1281     }
1283     $bits[] = html_writer::tag('span', $currentstatus,
1284             array('class' => 'quizopeningstatus', 'title' => implode(', ', $dates)));
1286     echo html_writer::tag('div', implode(' | ', $bits), array('class' => 'statusbar'));