Merge branch 'MDL-31080' of git://github.com/timhunt/moodle
[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_delete_previews($quiz);
252     quiz_update_sumgrades($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 . ' />';
408     $b = '<input name="moveselectedonpagebottom" type="text" size="2" ' .
409         $pagingdisabled . ' />';
411     $reordercontrols2top = '<div class="moveselectedonpage">' .
412         get_string('moveselectedonpage', 'quiz', $a) .
413         '<input type="submit" name="savechanges" value="' .
414         $strmove . '"  ' . $pagingdisabled . ' />' . '
415         <br /><input type="submit" name="savechanges" value="' .
416         $strreorderquestions . '" /></div>';
417     $reordercontrols2bottom = '<div class="moveselectedonpage">' .
418         '<input type="submit" name="savechanges" value="' .
419         $strreorderquestions . '" /><br />' .
420         get_string('moveselectedonpage', 'quiz', $b) .
421         '<input type="submit" name="savechanges" value="' .
422         $strmove . '"  ' . $pagingdisabled . ' /> ' . '</div>';
424     $reordercontrols3 = '<a href="javascript:select_all_in(\'FORM\', null, ' .
425             '\'quizquestions\');">' .
426             $strselectall . '</a> /';
427     $reordercontrols3.=    ' <a href="javascript:deselect_all_in(\'FORM\', ' .
428             'null, \'quizquestions\');">' .
429             $strselectnone . '</a>';
431     $reordercontrolstop = '<div class="reordercontrols">' .
432             $reordercontrolssetdefaultsubmit .
433             $reordercontrols1 . $reordercontrols2top . $reordercontrols3 . "</div>";
434     $reordercontrolsbottom = '<div class="reordercontrols">' .
435             $reordercontrolssetdefaultsubmit .
436             $reordercontrols2bottom . $reordercontrols1 . $reordercontrols3 . "</div>";
438     if ($reordertool) {
439         echo '<form method="post" action="edit.php" id="quizquestions"><div>';
441         echo html_writer::input_hidden_params($pageurl);
442         echo '<input type="hidden" name="sesskey" value="' . sesskey() . '" />';
444         echo $reordercontrolstop;
445     }
447     //the current question ordinal (no descriptions)
448     $qno = 1;
449     //the current question (includes questions and descriptions)
450     $questioncount = 0;
451     //the current page number in iteration
452     $pagecount = 0;
454     $pageopen = false;
456     $returnurl = $pageurl->out_as_local_url(false);
457     $questiontotalcount = count($order);
459     foreach ($order as $count => $qnum) {
461         $reordercheckbox = '';
462         $reordercheckboxlabel = '';
463         $reordercheckboxlabelclose = '';
465         // If the questiontype is missing change the question type
466         if ($qnum && !array_key_exists($qnum, $questions)) {
467             $fakequestion = new stdClass();
468             $fakequestion->id = $qnum;
469             $fakequestion->category = 0;
470             $fakequestion->qtype = 'missingtype';
471             $fakequestion->name = get_string('missingquestion', 'quiz');
472             $fakequestion->questiontext = ' ';
473             $fakequestion->questiontextformat = FORMAT_HTML;
474             $fakequestion->length = 1;
475             $questions[$qnum] = $fakequestion;
476             $quiz->grades[$qnum] = 0;
478         } else if ($qnum && !question_bank::qtype_exists($questions[$qnum]->qtype)) {
479             $questions[$qnum]->qtype = 'missingtype';
480         }
482         if ($qnum != 0 || ($qnum == 0 && !$pageopen)) {
483             //this is either a question or a page break after another
484             //        (no page is currently open)
485             if (!$pageopen) {
486                 //if no page is open, start display of a page
487                 $pagecount++;
488                 echo  '<div class="quizpage"><span class="pagetitle">' .
489                         get_string('page') . '&nbsp;' . $pagecount .
490                         '</span><div class="pagecontent">';
491                 $pageopen = true;
492             }
493             if ($qnum == 0  && $count < $questiontotalcount) {
494                 // This is the second successive page break. Tell the user the page is empty.
495                 echo '<div class="pagestatus">';
496                 print_string('noquestionsonpage', 'quiz');
497                 echo '</div>';
498                 if ($allowdelete) {
499                     echo '<div class="quizpagedelete">';
500                     echo $OUTPUT->action_icon($pageurl->out(true,
501                             array('deleteemptypage' => $count - 1, 'sesskey'=>sesskey())),
502                             new pix_icon('t/delete', $strremove),
503                             new component_action('click',
504                                     'M.core_scroll_manager.save_scroll_action'),
505                             array('title' => $strremove));
506                     echo '</div>';
507                 }
508             }
510             if ($qnum != 0) {
511                 $question = $questions[$qnum];
512                 $questionparams = array(
513                         'returnurl' => $returnurl,
514                         'cmid' => $quiz->cmid,
515                         'id' => $question->id);
516                 $questionurl = new moodle_url('/question/question.php',
517                         $questionparams);
518                 $questioncount++;
519                 //this is an actual question
521                 /* Display question start */
522                 ?>
523 <div class="question">
524     <div class="questioncontainer <?php echo $question->qtype; ?>">
525         <div class="qnum">
526                 <?php
527                 $reordercheckbox = '';
528                 $reordercheckboxlabel = '';
529                 $reordercheckboxlabelclose = '';
530                 if ($reordertool) {
531                     $reordercheckbox = '<input type="checkbox" name="s' . $question->id .
532                         '" id="s' . $question->id . '" />';
533                     $reordercheckboxlabel = '<label for="s' . $question->id . '">';
534                     $reordercheckboxlabelclose = '</label>';
535                 }
536                 if ($question->length == 0) {
537                     $qnodisplay = get_string('infoshort', 'quiz');
538                 } else if ($quiz->shufflequestions) {
539                     $qnodisplay = '?';
540                 } else {
541                     if ($qno > 999 || ($reordertool && $qno > 99)) {
542                         $qnodisplay = html_writer::tag('small', $qno);
543                     } else {
544                         $qnodisplay = $qno;
545                     }
546                     $qno += $question->length;
547                 }
548                 echo $reordercheckboxlabel . $qnodisplay . $reordercheckboxlabelclose .
549                         $reordercheckbox;
551                 ?>
552         </div>
553         <div class="content">
554             <div class="questioncontrols">
555                 <?php
556                 if ($count != 0) {
557                     if (!$hasattempts) {
558                         $upbuttonclass = '';
559                         if ($count >= $lastindex - 1) {
560                             $upbuttonclass = 'upwithoutdown';
561                         }
562                         echo $OUTPUT->action_icon($pageurl->out(true,
563                                 array('up' => $question->id, 'sesskey'=>sesskey())),
564                                 new pix_icon('t/up', $strmoveup),
565                                 new component_action('click',
566                                         'M.core_scroll_manager.save_scroll_action'),
567                                 array('title' => $strmoveup));
568                     }
570                 }
571                 if ($count < $lastindex - 1) {
572                     if (!$hasattempts) {
573                         echo $OUTPUT->action_icon($pageurl->out(true,
574                                 array('down' => $question->id, 'sesskey'=>sesskey())),
575                                 new pix_icon('t/down', $strmovedown),
576                                 new component_action('click',
577                                         'M.core_scroll_manager.save_scroll_action'),
578                                 array('title' => $strmovedown));
579                     }
580                 }
581                 if ($allowdelete && ($question->qtype == 'missingtype' ||
582                         question_has_capability_on($question, 'use', $question->category))) {
583                     // remove from quiz, not question delete.
584                     if (!$hasattempts) {
585                         echo $OUTPUT->action_icon($pageurl->out(true,
586                                 array('remove' => $question->id, 'sesskey'=>sesskey())),
587                                 new pix_icon('t/delete', $strremove),
588                                 new component_action('click',
589                                         'M.core_scroll_manager.save_scroll_action'),
590                                 array('title' => $strremove));
591                     }
592                 }
593                 ?>
594             </div><?php
595                 if (!in_array($question->qtype, array('description', 'missingtype')) && !$reordertool) {
596                     ?>
597 <div class="points">
598 <form method="post" action="edit.php" class="quizsavegradesform"><div>
599     <fieldset class="invisiblefieldset" style="display: block;">
600     <label for="<?php echo "inputq$question->id" ?>"><?php echo $strgrade; ?></label>:<br />
601     <input type="hidden" name="sesskey" value="<?php echo sesskey() ?>" />
602     <?php echo html_writer::input_hidden_params($pageurl); ?>
603     <input type="hidden" name="savechanges" value="save" />
604                     <?php
605                     echo '<input type="text" name="g' . $question->id .
606                             '" id="inputq' . $question->id .
607                             '" size="' . ($quiz->decimalpoints + 2) .
608                             '" value="' . (0 + $quiz->grades[$qnum]) .
609                             '" tabindex="' . ($lastindex + $qno) . '" />';
610                     ?>
611         <input type="submit" class="pointssubmitbutton" value="<?php echo $strsave; ?>" />
612     </fieldset>
613                     <?php
614                     if ($question->qtype == 'random') {
615                         echo '<a href="' . $questionurl->out() .
616                                 '" class="configurerandomquestion">' .
617                                 get_string("configurerandomquestion", "quiz") . '</a>';
618                     }
620                     ?>
621 </div>
622 </form>
624             </div>
625                     <?php
626                 } else if ($reordertool) {
627                     if ($qnum) {
628                         ?>
629 <div class="qorder">
630                         <?php
631                         echo '<input type="text" name="o' . $question->id .
632                                 '" size="2" value="' . (10*$count + 10) .
633                                 '" tabindex="' . ($lastindex + $qno) . '" />';
634                         ?>
635 </div>
636                         <?php
637                     }
638                 }
639                 ?>
640             <div class="questioncontentcontainer">
641                 <?php
642                 if ($question->qtype == 'random') { // it is a random question
643                     if (!$reordertool) {
644                         quiz_print_randomquestion($question, $pageurl, $quiz, $quiz_qbanktool);
645                     } else {
646                         quiz_print_randomquestion_reordertool($question, $pageurl, $quiz);
647                     }
648                 } else { // it is a single question
649                     if (!$reordertool) {
650                         quiz_print_singlequestion($question, $returnurl, $quiz);
651                     } else {
652                         quiz_print_singlequestion_reordertool($question, $returnurl, $quiz);
653                     }
654                 }
655                 ?>
656             </div>
657         </div>
658     </div>
659 </div>
661                 <?php
662             }
663         }
664         //a page break: end the existing page.
665         if ($qnum == 0) {
666             if ($pageopen) {
667                 if (!$reordertool && !($quiz->shufflequestions &&
668                         $count < $questiontotalcount - 1)) {
669                     quiz_print_pagecontrols($quiz, $pageurl, $pagecount,
670                             $hasattempts, $defaultcategoryobj);
671                 } else if ($count < $questiontotalcount - 1) {
672                     //do not include the last page break for reordering
673                     //to avoid creating a new extra page in the end
674                     echo '<input type="hidden" name="opg' . $pagecount . '" size="2" value="' .
675                             (10*$count + 10) . '" />';
676                 }
677                 echo "</div></div>";
679                 if (!$reordertool && !$quiz->shufflequestions) {
680                     echo $OUTPUT->container_start('addpage');
681                     $url = new moodle_url($pageurl->out_omit_querystring(),
682                             array('cmid' => $quiz->cmid, 'courseid' => $quiz->course,
683                                     'addpage' => $count, 'sesskey' => sesskey()));
684                     echo $OUTPUT->single_button($url, get_string('addpagehere', 'quiz'), 'post',
685                             array('disabled' => $hasattempts,
686                             'actions' => array(new component_action('click',
687                                     'M.core_scroll_manager.save_scroll_action'))));
688                     echo $OUTPUT->container_end();
689                 }
690                 $pageopen = false;
691                 $count++;
692             }
693         }
695     }
696     if ($reordertool) {
697         echo $reordercontrolsbottom;
698         echo '</div></form>';
699     }
702 /**
703  * Print all the controls for adding questions directly into the
704  * specific page in the edit tab of edit.php
705  *
706  * @param unknown_type $quiz
707  * @param unknown_type $pageurl
708  * @param unknown_type $page
709  * @param unknown_type $hasattempts
710  */
711 function quiz_print_pagecontrols($quiz, $pageurl, $page, $hasattempts, $defaultcategoryobj) {
712     global $CFG, $OUTPUT;
713     static $randombuttoncount = 0;
714     $randombuttoncount++;
715     echo '<div class="pagecontrols">';
717     // Get the current context
718     $thiscontext = get_context_instance(CONTEXT_COURSE, $quiz->course);
719     $contexts = new question_edit_contexts($thiscontext);
721     // Get the default category.
722     list($defaultcategoryid) = explode(',', $pageurl->param('cat'));
723     if (empty($defaultcategoryid)) {
724         $defaultcategoryid = $defaultcategoryobj->id;
725     }
727     // Create the url the question page will return to
728     $returnurladdtoquiz = new moodle_url($pageurl, array('addonpage' => $page));
730     // Print a button linking to the choose question type page.
731     $returnurladdtoquiz = $returnurladdtoquiz->out_as_local_url(false);
732     $newquestionparams = array('returnurl' => $returnurladdtoquiz,
733             'cmid' => $quiz->cmid, 'appendqnumstring' => 'addquestion');
734     create_new_question_button($defaultcategoryid, $newquestionparams,
735             get_string('addaquestion', 'quiz'),
736             get_string('createquestionandadd', 'quiz'), $hasattempts);
738     if ($hasattempts) {
739         $disabled = 'disabled="disabled"';
740     } else {
741         $disabled = '';
742     }
743     ?>
744     <div class="singlebutton">
745         <form class="randomquestionform" action="<?php echo $CFG->wwwroot;
746                 ?>/mod/quiz/addrandom.php" method="get">
747             <div>
748                 <input type="hidden" class="addonpage_formelement" name="addonpage" value="<?php
749                         echo $page; ?>" />
750                 <input type="hidden" name="cmid" value="<?php echo $quiz->cmid; ?>" />
751                 <input type="hidden" name="courseid" value="<?php echo $quiz->course; ?>" />
752                 <input type="hidden" name="category" value="<?php
753                         echo $pageurl->param('cat'); ?>" />
754                 <input type="hidden" name="returnurl" value="<?php
755                         echo s(str_replace($CFG->wwwroot, '', $pageurl->out(false))); ?>" />
756                 <input type="submit" id="addrandomdialoglaunch_<?php
757                         echo $randombuttoncount; ?>" value="<?php
758                         echo get_string('addarandomquestion', 'quiz'); ?>" <?php
759                         echo " $disabled"; ?> />
760             </div>
761         </form>
762     </div>
763     <?php echo $OUTPUT->help_icon('addarandomquestion', 'quiz'); ?>
764     <?php
765     echo "\n</div>";
768 /**
769  * Print a given single question in quiz for the edit tab of edit.php.
770  * Meant to be used from quiz_print_question_list()
771  *
772  * @param object $question A question object from the database questions table
773  * @param object $returnurl The url to get back to this page, for example after editing.
774  * @param object $quiz The quiz in the context of which the question is being displayed
775  */
776 function quiz_print_singlequestion($question, $returnurl, $quiz) {
777     echo '<div class="singlequestion ' . $question->qtype . '">';
778     echo quiz_question_edit_button($quiz->cmid, $question, $returnurl,
779             quiz_question_tostring($question) . ' ');
780     echo '<span class="questiontype">';
781     echo print_question_icon($question);
782     echo ' ' . question_bank::get_qtype_name($question->qtype) . '</span>';
783     echo '<span class="questionpreview">' .
784             quiz_question_preview_button($quiz, $question, true) . '</span>';
785     echo "</div>\n";
787 /**
788  * Print a given random question in quiz for the edit tab of edit.php.
789  * Meant to be used from quiz_print_question_list()
790  *
791  * @param object $question A question object from the database questions table
792  * @param object $questionurl The url of the question editing page as a moodle_url object
793  * @param object $quiz The quiz in the context of which the question is being displayed
794  * @param bool $quiz_qbanktool Indicate to this function if the question bank window open
795  */
796 function quiz_print_randomquestion(&$question, &$pageurl, &$quiz, $quiz_qbanktool) {
797     global $DB, $OUTPUT;
798     echo '<div class="quiz_randomquestion">';
800     if (!$category = $DB->get_record('question_categories',
801             array('id' => $question->category))) {
802         echo $OUTPUT->notification('Random question category not found!');
803         return;
804     }
806     echo '<div class="randomquestionfromcategory">';
807     echo print_question_icon($question);
808     print_random_option_icon($question);
809     echo ' ' . get_string('randomfromcategory', 'quiz') . '</div>';
811     $a = new stdClass();
812     $a->arrow = $OUTPUT->rarrow();
813     $strshowcategorycontents = get_string('showcategorycontents', 'quiz', $a);
815     $openqbankurl = $pageurl->out(true, array('qbanktool' => 1,
816             'cat' => $category->id . ',' . $category->contextid));
817     $linkcategorycontents = ' <a href="' . $openqbankurl . '">' . $strshowcategorycontents . '</a>';
819     echo '<div class="randomquestioncategory">';
820     echo '<a href="' . $openqbankurl . '" title="' . $strshowcategorycontents . '">' .
821             $category->name . '</a>';
822     echo '<span class="questionpreview">' .
823             quiz_question_preview_button($quiz, $question, true) . '</span>';
824     echo '</div>';
826     $questionids = question_bank::get_qtype('random')->get_available_questions_from_category(
827             $category->id, $question->questiontext == '1', '0');
828     $questioncount = count($questionids);
830     echo '<div class="randomquestionqlist">';
831     if ($questioncount == 0) {
832         // No questions in category, give an error plus instructions
833         echo '<span class="error">';
834         print_string('noquestionsnotinuse', 'quiz');
835         echo '</span>';
836         echo '<br />';
838         // Embed the link into the string with instructions
839         $a = new stdClass();
840         $a->catname = '<strong>' . $category->name . '</strong>';
841         $a->link = $linkcategorycontents;
842         echo get_string('addnewquestionsqbank', 'quiz', $a);
844     } else {
845         // Category has questions
847         // Get a sample from the database,
848         $questionidstoshow = array_slice($questionids, 0, NUM_QS_TO_SHOW_IN_RANDOM);
849         $questionstoshow = $DB->get_records_list('question', 'id', $questionidstoshow,
850                 '', 'id, qtype, name, questiontext, questiontextformat');
852         // list them,
853         echo '<ul>';
854         foreach ($questionstoshow as $question) {
855             echo '<li>' . quiz_question_tostring($question, true) . '</li>';
856         }
858         // and then display the total number.
859         echo '<li class="totalquestionsinrandomqcategory">';
860         if ($questioncount > NUM_QS_TO_SHOW_IN_RANDOM) {
861             echo '... ';
862         }
863         print_string('totalquestionsinrandomqcategory', 'quiz', $questioncount);
864         echo ' ' . $linkcategorycontents;
865         echo '</li>';
866         echo '</ul>';
867     }
869     echo '</div>';
870     echo '<div class="randomquestioncategorycount">';
871     echo '</div>';
872     echo '</div>';
875 /**
876  * Print a given single question in quiz for the reordertool tab of edit.php.
877  * Meant to be used from quiz_print_question_list()
878  *
879  * @param object $question A question object from the database questions table
880  * @param object $questionurl The url of the question editing page as a moodle_url object
881  * @param object $quiz The quiz in the context of which the question is being displayed
882  */
883 function quiz_print_singlequestion_reordertool($question, $returnurl, $quiz) {
884     echo '<div class="singlequestion ' . $question->qtype . '">';
885     echo '<label for="s' . $question->id . '">';
886     echo print_question_icon($question);
887     echo ' ' . quiz_question_tostring($question);
888     echo '</label>';
889     echo '<span class="questionpreview">' .
890             quiz_question_action_icons($quiz, $quiz->cmid, $question, $returnurl) . '</span>';
891     echo "</div>\n";
894 /**
895  * Print a given random question in quiz for the reordertool tab of edit.php.
896  * Meant to be used from quiz_print_question_list()
897  *
898  * @param object $question A question object from the database questions table
899  * @param object $questionurl The url of the question editing page as a moodle_url object
900  * @param object $quiz The quiz in the context of which the question is being displayed
901  */
902 function quiz_print_randomquestion_reordertool(&$question, &$pageurl, &$quiz) {
903     global $DB, $OUTPUT;
905     // Load the category, and the number of available questions in it.
906     if (!$category = $DB->get_record('question_categories', array('id' => $question->category))) {
907         echo $OUTPUT->notification('Random question category not found!');
908         return;
909     }
910     $questioncount = count(question_bank::get_qtype(
911             'random')->get_available_questions_from_category(
912             $category->id, $question->questiontext == '1', '0'));
914     $reordercheckboxlabel = '<label for="s' . $question->id . '">';
915     $reordercheckboxlabelclose = '</label>';
917     echo '<div class="quiz_randomquestion">';
918     echo '<div class="randomquestionfromcategory">';
919     echo $reordercheckboxlabel;
920     echo print_question_icon($question);
921     print_random_option_icon($question);
923     if ($questioncount == 0) {
924         echo '<span class="error">';
925         print_string('empty', 'quiz');
926         echo '</span> ';
927     }
929     print_string('random', 'quiz');
930     echo ": $reordercheckboxlabelclose</div>";
932     echo '<div class="randomquestioncategory">';
933     echo $reordercheckboxlabel . $category->name . $reordercheckboxlabelclose;
934     echo '<span class="questionpreview">';
935     echo quiz_question_preview_button($quiz, $question, false);
936     echo '</span>';
937     echo "</div>";
939     echo '<div class="randomquestioncategorycount">';
940     echo '</div>';
941     echo '</div>';
944 /**
945  * Print an icon to indicate the 'include subcategories' state of a random question.
946  * @param $question the random question.
947  */
948 function print_random_option_icon($question) {
949     global $OUTPUT;
950     if (!empty($question->questiontext)) {
951         $icon = 'withsubcat';
952         $tooltip = get_string('randomwithsubcat', 'quiz');
953     } else {
954         $icon = 'nosubcat';
955         $tooltip = get_string('randomnosubcat', 'quiz');
956     }
957     echo '<img src="' . $OUTPUT->pix_url('i/' . $icon) . '" alt="' .
958             $tooltip . '" title="' . $tooltip . '" class="uihint" />';
961 /**
962  * Creates a textual representation of a question for display.
963  *
964  * @param object $question A question object from the database questions table
965  * @param bool $showicon If true, show the question's icon with the question. False by default.
966  * @param bool $showquestiontext If true (default), show question text after question name.
967  *       If false, show only question name.
968  * @param bool $return If true (default), return the output. If false, print it.
969  */
970 function quiz_question_tostring($question, $showicon = false,
971         $showquestiontext = true, $return = true) {
972     global $COURSE;
973     $result = '';
974     $result .= '<span class="questionname">';
975     if ($showicon) {
976         $result .= print_question_icon($question, true);
977         echo ' ';
978     }
979     $result .= shorten_text(format_string($question->name), 200) . '</span>';
980     if ($showquestiontext) {
981         $formatoptions = new stdClass();
982         $formatoptions->noclean = true;
983         $formatoptions->para = false;
984         $questiontext = strip_tags(format_text($question->questiontext,
985                 $question->questiontextformat,
986                 $formatoptions, $COURSE->id));
987         $questiontext = shorten_text($questiontext, 200);
988         $result .= '<span class="questiontext">';
989         if (!empty($questiontext)) {
990             $result .= $questiontext;
991         } else {
992             $result .= '<span class="error">';
993             $result .= get_string('questiontextisempty', 'quiz');
994             $result .= '</span>';
995         }
996         $result .= '</span>';
997     }
998     if ($return) {
999         return $result;
1000     } else {
1001         echo $result;
1002     }
1005 /**
1006  * A column type for the add this question to the quiz.
1007  *
1008  * @copyright  2009 Tim Hunt
1009  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1010  */
1011 class question_bank_add_to_quiz_action_column extends question_bank_action_column_base {
1012     protected $stradd;
1014     public function init() {
1015         parent::init();
1016         $this->stradd = get_string('addtoquiz', 'quiz');
1017     }
1019     public function get_name() {
1020         return 'addtoquizaction';
1021     }
1023     protected function display_content($question, $rowclasses) {
1024         // for RTL languages: switch right and left arrows
1025         if (right_to_left()) {
1026             $movearrow = 't/removeright';
1027         } else {
1028             $movearrow = 't/moveleft';
1029         }
1030         $this->print_icon($movearrow, $this->stradd, $this->qbank->add_to_quiz_url($question->id));
1031     }
1033     public function get_required_fields() {
1034         return array('q.id');
1035     }
1038 /**
1039  * A column type for the name followed by the start of the question text.
1040  *
1041  * @copyright  2009 Tim Hunt
1042  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1043  */
1044 class question_bank_question_name_text_column extends question_bank_question_name_column {
1045     public function get_name() {
1046         return 'questionnametext';
1047     }
1049     protected function display_content($question, $rowclasses) {
1050         echo '<div>';
1051         $labelfor = $this->label_for($question);
1052         if ($labelfor) {
1053             echo '<label for="' . $labelfor . '">';
1054         }
1055         echo quiz_question_tostring($question, false, true, true);
1056         if ($labelfor) {
1057             echo '</label>';
1058         }
1059         echo '</div>';
1060     }
1062     public function get_required_fields() {
1063         $fields = parent::get_required_fields();
1064         $fields[] = 'q.questiontext';
1065         $fields[] = 'q.questiontextformat';
1066         return $fields;
1067     }
1070 /**
1071  * Subclass to customise the view of the question bank for the quiz editing screen.
1072  *
1073  * @copyright  2009 Tim Hunt
1074  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1075  */
1076 class quiz_question_bank_view extends question_bank_view {
1077     protected $quizhasattempts = false;
1078     /** @var object the quiz settings. */
1079     protected $quiz = false;
1081     /**
1082      * Constructor
1083      * @param question_edit_contexts $contexts
1084      * @param moodle_url $pageurl
1085      * @param object $course course settings
1086      * @param object $cm activity settings.
1087      * @param object $quiz quiz settings.
1088      */
1089     public function __construct($contexts, $pageurl, $course, $cm, $quiz) {
1090         parent::__construct($contexts, $pageurl, $course, $cm);
1091         $this->quiz = $quiz;
1092     }
1094     protected function known_field_types() {
1095         $types = parent::known_field_types();
1096         $types[] = new question_bank_add_to_quiz_action_column($this);
1097         $types[] = new question_bank_question_name_text_column($this);
1098         return $types;
1099     }
1101     protected function wanted_columns() {
1102         return array('addtoquizaction', 'checkbox', 'qtype', 'questionnametext',
1103                 'editaction', 'previewaction');
1104     }
1106     protected function default_sort() {
1107         return array('qtype' => 1, 'questionnametext' => 1);
1108     }
1110     /**
1111      * Let the question bank display know whether the quiz has been attempted,
1112      * hence whether some bits of UI, like the add this question to the quiz icon,
1113      * should be displayed.
1114      * @param bool $quizhasattempts whether the quiz has attempts.
1115      */
1116     public function set_quiz_has_attempts($quizhasattempts) {
1117         $this->quizhasattempts = $quizhasattempts;
1118         if ($quizhasattempts && isset($this->visiblecolumns['addtoquizaction'])) {
1119             unset($this->visiblecolumns['addtoquizaction']);
1120         }
1121     }
1123     public function preview_question_url($question) {
1124         return quiz_question_preview_url($this->quiz, $question);
1125     }
1127     public function add_to_quiz_url($questionid) {
1128         global $CFG;
1129         $params = $this->baseurl->params();
1130         $params['addquestion'] = $questionid;
1131         $params['sesskey'] = sesskey();
1132         return new moodle_url('/mod/quiz/edit.php', $params);
1133     }
1135     public function display($tabname, $page, $perpage, $cat,
1136             $recurse, $showhidden, $showquestiontext) {
1137         global $OUTPUT;
1138         if ($this->process_actions_needing_ui()) {
1139             return;
1140         }
1142         // Display the current category.
1143         if (!$category = $this->get_current_category($cat)) {
1144             return;
1145         }
1146         $this->print_category_info($category);
1148         echo $OUTPUT->box_start('generalbox questionbank');
1150         $this->display_category_form($this->contexts->having_one_edit_tab_cap($tabname),
1151                 $this->baseurl, $cat);
1153         // continues with list of questions
1154         $this->display_question_list($this->contexts->having_one_edit_tab_cap($tabname),
1155                 $this->baseurl, $cat, $this->cm, $recurse, $page,
1156                 $perpage, $showhidden, $showquestiontext,
1157                 $this->contexts->having_cap('moodle/question:add'));
1159         $this->display_options($recurse, $showhidden, $showquestiontext);
1160         echo $OUTPUT->box_end();
1161     }
1163     protected function print_choose_category_message($categoryandcontext) {
1164         global $OUTPUT;
1165         echo $OUTPUT->box_start('generalbox questionbank');
1166         $this->display_category_form($this->contexts->having_one_edit_tab_cap('edit'),
1167                 $this->baseurl, $categoryandcontext);
1168         echo "<p style=\"text-align:center;\"><b>";
1169         print_string('selectcategoryabove', 'question');
1170         echo "</b></p>";
1171         echo $OUTPUT->box_end();
1172     }
1174     protected function print_category_info($category) {
1175         $formatoptions = new stdClass();
1176         $formatoptions->noclean = true;
1177         $strcategory = get_string('category', 'quiz');
1178         echo '<div class="categoryinfo"><div class="categorynamefieldcontainer">' .
1179                 $strcategory;
1180         echo ': <span class="categorynamefield">';
1181         echo shorten_text(strip_tags(format_string($category->name)), 60);
1182         echo '</span></div><div class="categoryinfofieldcontainer">' .
1183                 '<span class="categoryinfofield">';
1184         echo shorten_text(strip_tags(format_text($category->info, $category->infoformat,
1185                 $formatoptions, $this->course->id)), 200);
1186         echo '</span></div></div>';
1187     }
1189     protected function display_options($recurse, $showhidden, $showquestiontext) {
1190         echo '<form method="get" action="edit.php" id="displayoptions">';
1191         echo "<fieldset class='invisiblefieldset'>";
1192         echo html_writer::input_hidden_params($this->baseurl,
1193                 array('recurse', 'showhidden', 'qbshowtext'));
1194         $this->display_category_form_checkbox('recurse', $recurse,
1195                 get_string('includesubcategories', 'question'));
1196         $this->display_category_form_checkbox('showhidden', $showhidden,
1197                 get_string('showhidden', 'question'));
1198         echo '<noscript><div class="centerpara"><input type="submit" value="' .
1199                 get_string('go') . '" />';
1200         echo '</div></noscript></fieldset></form>';
1201     }
1204 /**
1205  * Prints the form for setting a quiz' overall grade
1206  *
1207  * @param object $quiz The quiz object of the quiz in question
1208  * @param object $pageurl The url of the current page with the parameters required
1209  *     for links returning to the current page, as a moodle_url object
1210  * @param int $tabindex The tabindex to start from for the form elements created
1211  * @return int The tabindex from which the calling page can continue, that is,
1212  *      the last value used +1.
1213  */
1214 function quiz_print_grading_form($quiz, $pageurl, $tabindex) {
1215     global $USER, $OUTPUT;
1216     $strsave = get_string('save', 'quiz');
1217     echo '<form method="post" action="edit.php" class="quizsavegradesform"><div>';
1218     echo '<fieldset class="invisiblefieldset" style="display: block;">';
1219     echo "<input type=\"hidden\" name=\"sesskey\" value=\"" . sesskey() . "\" />";
1220     echo html_writer::input_hidden_params($pageurl);
1221     $a = '<input type="text" id="inputmaxgrade" name="maxgrade" size="' .
1222             ($quiz->decimalpoints + 2) . '" tabindex="' . $tabindex
1223          . '" value="' . quiz_format_grade($quiz, $quiz->grade) . '" />';
1224     echo '<label for="inputmaxgrade">' . get_string('maximumgradex', '', $a) . "</label>";
1225     echo '<input type="hidden" name="savechanges" value="save" />';
1226     echo '<input type="submit" value="' . $strsave . '" />';
1227     echo '</fieldset>';
1228     echo "</div></form>\n";
1229     return $tabindex + 1;
1232 /**
1233  * Print the status bar
1234  *
1235  * @param object $quiz The quiz object of the quiz in question
1236  */
1237 function quiz_print_status_bar($quiz) {
1238     global $CFG;
1240     $bits = array();
1242     $bits[] = html_writer::tag('span',
1243             get_string('totalpointsx', 'quiz', quiz_format_grade($quiz, $quiz->sumgrades)),
1244             array('class' => 'totalpoints'));
1246     $bits[] = html_writer::tag('span',
1247             get_string('numquestionsx', 'quiz', quiz_number_of_questions_in_quiz($quiz->questions)),
1248             array('class' => 'numberofquestions'));
1250     $timenow = time();
1252     // Exact open and close dates for the tool-tip.
1253     $dates = array();
1254     if ($quiz->timeopen > 0) {
1255         if ($timenow > $quiz->timeopen) {
1256             $dates[] = get_string('quizopenedon', 'quiz', userdate($quiz->timeopen));
1257         } else {
1258             $dates[] = get_string('quizwillopen', 'quiz', userdate($quiz->timeopen));
1259         }
1260     }
1261     if ($quiz->timeclose > 0) {
1262         if ($timenow > $quiz->timeclose) {
1263             $dates[] = get_string('quizclosed', 'quiz', userdate($quiz->timeclose));
1264         } else {
1265             $dates[] = get_string('quizcloseson', 'quiz', userdate($quiz->timeclose));
1266         }
1267     }
1268     if (empty($dates)) {
1269         $dates[] = get_string('alwaysavailable', 'quiz');
1270     }
1271     $tooltip = implode(', ', $dates);;
1273     // Brief summary on the page.
1274     if ($timenow < $quiz->timeopen) {
1275         $currentstatus = get_string('quizisclosedwillopen', 'quiz',
1276                 userdate($quiz->timeopen, get_string('strftimedatetimeshort', 'langconfig')));
1277     } else if ($quiz->timeclose && $timenow <= $quiz->timeclose) {
1278         $currentstatus = get_string('quizisopenwillclose', 'quiz',
1279                 userdate($quiz->timeclose, get_string('strftimedatetimeshort', 'langconfig')));
1280     } else if ($quiz->timeclose && $timenow > $quiz->timeclose) {
1281         $currentstatus = get_string('quizisclosed', 'quiz');
1282     } else {
1283         $currentstatus = get_string('quizisopen', 'quiz');
1284     }
1286     $bits[] = html_writer::tag('span', $currentstatus,
1287             array('class' => 'quizopeningstatus', 'title' => implode(', ', $dates)));
1289     echo html_writer::tag('div', implode(' | ', $bits), array('class' => 'statusbar'));