MDL-23919 mod_data: completionentries can be null
[moodle.git] / mod / quiz / classes / output / edit_renderer.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/>.
17 /**
18  * Renderer outputting the quiz editing UI.
19  *
20  * @package mod_quiz
21  * @copyright 2013 The Open University.
22  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 namespace mod_quiz\output;
26 defined('MOODLE_INTERNAL') || die();
28 use \mod_quiz\structure;
29 use \html_writer;
30 use renderable;
32 /**
33  * Renderer outputting the quiz editing UI.
34  *
35  * @copyright 2013 The Open University.
36  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
37  * @since Moodle 2.7
38  */
39 class edit_renderer extends \plugin_renderer_base {
41     /**
42      * Render the edit page
43      *
44      * @param \quiz $quizobj object containing all the quiz settings information.
45      * @param structure $structure object containing the structure of the quiz.
46      * @param \question_edit_contexts $contexts the relevant question bank contexts.
47      * @param \moodle_url $pageurl the canonical URL of this page.
48      * @param array $pagevars the variables from {@link question_edit_setup()}.
49      * @return string HTML to output.
50      */
51     public function edit_page(\quiz $quizobj, structure $structure,
52             \question_edit_contexts $contexts, \moodle_url $pageurl, array $pagevars) {
53         $output = '';
55         // Page title.
56         $output .= $this->heading_with_help(get_string('editingquizx', 'quiz',
57                 format_string($quizobj->get_quiz_name())), 'editingquiz', 'quiz', '',
58                 get_string('basicideasofquiz', 'quiz'), 2);
60         // Information at the top.
61         $output .= $this->quiz_state_warnings($structure);
62         $output .= $this->quiz_information($structure);
63         $output .= $this->maximum_grade_input($structure, $pageurl);
64         $output .= $this->repaginate_button($structure, $pageurl);
65         $output .= $this->total_marks($quizobj->get_quiz());
67         // Show the questions organised into sections and pages.
68         $output .= $this->start_section_list($structure);
70         foreach ($structure->get_sections() as $section) {
71             $output .= $this->start_section($structure, $section);
72             $output .= $this->questions_in_section($structure, $section, $contexts, $pagevars, $pageurl);
74             if ($structure->is_last_section($section)) {
75                 $output .= \html_writer::start_div('last-add-menu');
76                 $output .= html_writer::tag('span', $this->add_menu_actions($structure, 0,
77                         $pageurl, $contexts, $pagevars), array('class' => 'add-menu-outer'));
78                 $output .= \html_writer::end_div();
79             }
81             $output .= $this->end_section();
82         }
84         $output .= $this->end_section_list();
86         // Initialise the JavaScript.
87         $this->initialise_editing_javascript($structure, $contexts, $pagevars, $pageurl);
89         // Include the contents of any other popups required.
90         if ($structure->can_be_edited()) {
91             $popups = '';
93             $popups .= $this->question_bank_loading();
94             $this->page->requires->yui_module('moodle-mod_quiz-quizquestionbank',
95                     'M.mod_quiz.quizquestionbank.init',
96                     array('class' => 'questionbank', 'cmid' => $structure->get_cmid()));
98             $popups .= $this->random_question_form($pageurl, $contexts, $pagevars);
99             $this->page->requires->yui_module('moodle-mod_quiz-randomquestion',
100                     'M.mod_quiz.randomquestion.init');
102             $output .= html_writer::div($popups, 'mod_quiz_edit_forms');
104             // Include the question chooser.
105             $output .= $this->question_chooser();
106         }
108         return $output;
109     }
111     /**
112      * Render any warnings that might be required about the state of the quiz,
113      * e.g. if it has been attempted, or if the shuffle questions option is
114      * turned on.
115      *
116      * @param structure $structure the quiz structure.
117      * @return string HTML to output.
118      */
119     public function quiz_state_warnings(structure $structure) {
120         $warnings = $structure->get_edit_page_warnings();
122         if (empty($warnings)) {
123             return '';
124         }
126         $output = array();
127         foreach ($warnings as $warning) {
128             $output[] = \html_writer::tag('p', $warning);
129         }
130         return $this->box(implode("\n", $output), 'statusdisplay');
131     }
133     /**
134      * Render the status bar.
135      *
136      * @param structure $structure the quiz structure.
137      * @return string HTML to output.
138      */
139     public function quiz_information(structure $structure) {
140         list($currentstatus, $explanation) = $structure->get_dates_summary();
142         $output = html_writer::span(
143                     get_string('numquestionsx', 'quiz', $structure->get_question_count()),
144                     'numberofquestions') . ' | ' .
145                 html_writer::span($currentstatus, 'quizopeningstatus',
146                     array('title' => $explanation));
148         return html_writer::div($output, 'statusbar');
149     }
151     /**
152      * Render the form for setting a quiz' overall grade
153      *
154      * @param structure $structure the quiz structure.
155      * @param \moodle_url $pageurl the canonical URL of this page.
156      * @return string HTML to output.
157      */
158     public function maximum_grade_input($structure, \moodle_url $pageurl) {
159         $output = '';
160         $output .= html_writer::start_div('maxgrade');
161         $output .= html_writer::start_tag('form', array('method' => 'post', 'action' => 'edit.php',
162                 'class' => 'quizsavegradesform form-inline'));
163         $output .= html_writer::start_tag('fieldset', array('class' => 'invisiblefieldset'));
164         $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey()));
165         $output .= html_writer::input_hidden_params($pageurl);
166         $output .= html_writer::tag('label', get_string('maximumgrade') . ' ',
167                 array('for' => 'inputmaxgrade'));
168         $output .= html_writer::empty_tag('input', array('type' => 'text', 'id' => 'inputmaxgrade',
169                 'name' => 'maxgrade', 'size' => ($structure->get_decimal_places_for_grades() + 2),
170                 'value' => $structure->formatted_quiz_grade(),
171                 'class' => 'form-control'));
172         $output .= html_writer::empty_tag('input', array('type' => 'submit', 'class' => 'btn btn-secondary m-l-1',
173                 'name' => 'savechanges', 'value' => get_string('save', 'quiz')));
174         $output .= html_writer::end_tag('fieldset');
175         $output .= html_writer::end_tag('form');
176         $output .= html_writer::end_tag('div');
177         return $output;
178     }
180     /**
181      * Return the repaginate button
182      * @param structure $structure the structure of the quiz being edited.
183      * @param \moodle_url $pageurl the canonical URL of this page.
184      * @return string HTML to output.
185      */
186     protected function repaginate_button(structure $structure, \moodle_url $pageurl) {
188         $header = html_writer::tag('span', get_string('repaginatecommand', 'quiz'), array('class' => 'repaginatecommand'));
189         $form = $this->repaginate_form($structure, $pageurl);
190         $containeroptions = array(
191                 'class'  => 'rpcontainerclass',
192                 'cmid'   => $structure->get_cmid(),
193                 'header' => $header,
194                 'form'   => $form,
195         );
197         $buttonoptions = array(
198             'type'  => 'submit',
199             'name'  => 'repaginate',
200             'id'    => 'repaginatecommand',
201             'value' => get_string('repaginatecommand', 'quiz'),
202             'class' => 'btn btn-secondary m-b-1',
203         );
204         if (!$structure->can_be_repaginated()) {
205             $buttonoptions['disabled'] = 'disabled';
206         } else {
207             $this->page->requires->yui_module('moodle-mod_quiz-repaginate', 'M.mod_quiz.repaginate.init');
208         }
210         return html_writer::tag('div',
211                 html_writer::empty_tag('input', $buttonoptions), $containeroptions);
212     }
214     /**
215      * Return the repaginate form
216      * @param structure $structure the structure of the quiz being edited.
217      * @param \moodle_url $pageurl the canonical URL of this page.
218      * @return string HTML to output.
219      */
220     protected function repaginate_form(structure $structure, \moodle_url $pageurl) {
221         $perpage = array();
222         $perpage[0] = get_string('allinone', 'quiz');
223         for ($i = 1; $i <= 50; ++$i) {
224             $perpage[$i] = $i;
225         }
227         $hiddenurl = clone($pageurl);
228         $hiddenurl->param('sesskey', sesskey());
230         $select = html_writer::select($perpage, 'questionsperpage',
231                 $structure->get_questions_per_page(), false, array('class' => 'custom-select'));
233         $buttonattributes = array(
234             'type' => 'submit',
235             'name' => 'repaginate',
236             'value' => get_string('go'),
237             'class' => 'btn btn-secondary m-l-1'
238         );
240         $formcontent = html_writer::tag('form', html_writer::div(
241                     html_writer::input_hidden_params($hiddenurl) .
242                     get_string('repaginate', 'quiz', $select) .
243                     html_writer::empty_tag('input', $buttonattributes)
244                 ), array('action' => 'edit.php', 'method' => 'post'));
246         return html_writer::div($formcontent, '', array('id' => 'repaginatedialog'));
247     }
249     /**
250      * Render the total marks available for the quiz.
251      *
252      * @param \stdClass $quiz the quiz settings from the database.
253      * @return string HTML to output.
254      */
255     public function total_marks($quiz) {
256         $totalmark = html_writer::span(quiz_format_grade($quiz, $quiz->sumgrades), 'mod_quiz_summarks');
257         return html_writer::tag('span',
258                 get_string('totalmarksx', 'quiz', $totalmark),
259                 array('class' => 'totalpoints'));
260     }
262     /**
263      * Generate the starting container html for the start of a list of sections
264      * @param structure $structure the structure of the quiz being edited.
265      * @return string HTML to output.
266      */
267     protected function start_section_list(structure $structure) {
268         $class = 'slots';
269         if ($structure->get_section_count() == 1) {
270             $class .= ' only-one-section';
271         }
272         return html_writer::start_tag('ul', array('class' => $class));
273     }
275     /**
276      * Generate the closing container html for the end of a list of sections
277      * @return string HTML to output.
278      */
279     protected function end_section_list() {
280         return html_writer::end_tag('ul');
281     }
283     /**
284      * Display the start of a section, before the questions.
285      *
286      * @param structure $structure the structure of the quiz being edited.
287      * @param \stdClass $section The quiz_section entry from DB
288      * @return string HTML to output.
289      */
290     protected function start_section($structure, $section) {
292         $output = '';
294         $sectionstyle = '';
295         if ($structure->is_only_one_slot_in_section($section)) {
296             $sectionstyle = ' only-has-one-slot';
297         }
299         $output .= html_writer::start_tag('li', array('id' => 'section-'.$section->id,
300             'class' => 'section main clearfix'.$sectionstyle, 'role' => 'region',
301             'aria-label' => $section->heading));
303         $output .= html_writer::start_div('content');
305         $output .= html_writer::start_div('section-heading');
307         $headingtext = $this->heading(html_writer::span(
308                 html_writer::span($section->heading, 'instancesection'), 'sectioninstance'), 3);
310         if (!$structure->can_be_edited()) {
311             $editsectionheadingicon = '';
312         } else {
313             $editsectionheadingicon = html_writer::link(new \moodle_url('#'),
314                 $this->pix_icon('t/editstring', get_string('sectionheadingedit', 'quiz', $section->heading),
315                         'moodle', array('class' => 'editicon visibleifjs')),
316                         array('class' => 'editing_section', 'data-action' => 'edit_section_title'));
317         }
318         $output .= html_writer::div($headingtext . $editsectionheadingicon, 'instancesectioncontainer');
320         if (!$structure->is_first_section($section) && $structure->can_be_edited()) {
321             $output .= $this->section_remove_icon($section);
322         }
323         $output .= $this->section_shuffle_questions($structure, $section);
325         $output .= html_writer::end_div($output, 'section-heading');
327         return $output;
328     }
330     /**
331      * Display a checkbox for shuffling question within a section.
332      *
333      * @param structure $structure object containing the structure of the quiz.
334      * @param \stdClass $section data from the quiz_section table.
335      * @return string HTML to output.
336      */
337     public function section_shuffle_questions(structure $structure, $section) {
338         $checkboxattributes = array(
339             'type' => 'checkbox',
340             'id' => 'shuffle-' . $section->id,
341             'value' => 1,
342             'data-action' => 'shuffle_questions',
343             'class' => 'cm-edit-action',
344         );
346         if (!$structure->can_be_edited()) {
347             $checkboxattributes['disabled'] = 'disabled';
348         }
349         if ($section->shufflequestions) {
350             $checkboxattributes['checked'] = 'checked';
351         }
353         if ($structure->is_first_section($section)) {
354             $help = $this->help_icon('shufflequestions', 'quiz');
355         } else {
356             $help = '';
357         }
359         $progressspan = html_writer::span('', 'shuffle-progress');
360         $checkbox = html_writer::empty_tag('input', $checkboxattributes);
361         $label = html_writer::label(get_string('shufflequestions', 'quiz') . ' ' . $help,
362                 $checkboxattributes['id'], false);
363         return html_writer::span($progressspan . $checkbox . $label,
364                 'instanceshufflequestions', array('data-action' => 'shuffle_questions'));
365     }
367     /**
368      * Display the end of a section, after the questions.
369      *
370      * @return string HTML to output.
371      */
372     protected function end_section() {
373         $output = html_writer::end_tag('div');
374         $output .= html_writer::end_tag('li');
376         return $output;
377     }
379     /**
380      * Render an icon to remove a section from the quiz.
381      *
382      * @param object $section the section to be removed.
383      * @return string HTML to output.
384      */
385     public function section_remove_icon($section) {
386         $title = get_string('sectionheadingremove', 'quiz', $section->heading);
387         $url = new \moodle_url('/mod/quiz/edit.php',
388                 array('sesskey' => sesskey(), 'removesection' => '1', 'sectionid' => $section->id));
389         $image = $this->pix_icon('t/delete', $title);
390         return $this->action_link($url, $image, null, array(
391                 'class' => 'cm-edit-action editing_delete', 'data-action' => 'deletesection'));
392     }
394     /**
395      * Renders HTML to display the questions in a section of the quiz.
396      *
397      * This function calls {@link core_course_renderer::quiz_section_question()}
398      *
399      * @param structure $structure object containing the structure of the quiz.
400      * @param \stdClass $section information about the section.
401      * @param \question_edit_contexts $contexts the relevant question bank contexts.
402      * @param array $pagevars the variables from {@link \question_edit_setup()}.
403      * @param \moodle_url $pageurl the canonical URL of this page.
404      * @return string HTML to output.
405      */
406     public function questions_in_section(structure $structure, $section,
407             $contexts, $pagevars, $pageurl) {
409         $output = '';
410         foreach ($structure->get_slots_in_section($section->id) as $slot) {
411             $output .= $this->question_row($structure, $slot, $contexts, $pagevars, $pageurl);
412         }
413         return html_writer::tag('ul', $output, array('class' => 'section img-text'));
414     }
416     /**
417      * Displays one question with the surrounding controls.
418      *
419      * @param structure $structure object containing the structure of the quiz.
420      * @param int $slot which slot we are outputting.
421      * @param \question_edit_contexts $contexts the relevant question bank contexts.
422      * @param array $pagevars the variables from {@link \question_edit_setup()}.
423      * @param \moodle_url $pageurl the canonical URL of this page.
424      * @return string HTML to output.
425      */
426     public function question_row(structure $structure, $slot, $contexts, $pagevars, $pageurl) {
427         $output = '';
429         $output .= $this->page_row($structure, $slot, $contexts, $pagevars, $pageurl);
431         // Page split/join icon.
432         $joinhtml = '';
433         if ($structure->can_be_edited() && !$structure->is_last_slot_in_quiz($slot) &&
434                                             !$structure->is_last_slot_in_section($slot)) {
435             $joinhtml = $this->page_split_join_button($structure, $slot);
436         }
437         // Question HTML.
438         $questionhtml = $this->question($structure, $slot, $pageurl);
439         $qtype = $structure->get_question_type_for_slot($slot);
440         $questionclasses = 'activity ' . $qtype . ' qtype_' . $qtype . ' slot';
442         $output .= html_writer::tag('li', $questionhtml . $joinhtml,
443                 array('class' => $questionclasses, 'id' => 'slot-' . $structure->get_slot_id_for_slot($slot),
444                         'data-canfinish' => $structure->can_finish_during_the_attempt($slot)));
446         return $output;
447     }
449     /**
450      * Displays one question with the surrounding controls.
451      *
452      * @param structure $structure object containing the structure of the quiz.
453      * @param int $slot the first slot on the page we are outputting.
454      * @param \question_edit_contexts $contexts the relevant question bank contexts.
455      * @param array $pagevars the variables from {@link \question_edit_setup()}.
456      * @param \moodle_url $pageurl the canonical URL of this page.
457      * @return string HTML to output.
458      */
459     public function page_row(structure $structure, $slot, $contexts, $pagevars, $pageurl) {
460         $output = '';
462         $pagenumber = $structure->get_page_number_for_slot($slot);
464         // Put page in a heading for accessibility and styling.
465         $page = $this->heading(get_string('page') . ' ' . $pagenumber, 4);
467         if ($structure->is_first_slot_on_page($slot)) {
468             // Add the add-menu at the page level.
469             $addmenu = html_writer::tag('span', $this->add_menu_actions($structure,
470                     $pagenumber, $pageurl, $contexts, $pagevars),
471                     array('class' => 'add-menu-outer'));
473             $addquestionform = $this->add_question_form($structure,
474                     $pagenumber, $pageurl, $pagevars);
476             $output .= html_writer::tag('li', $page . $addmenu . $addquestionform,
477                     array('class' => 'pagenumber activity yui3-dd-drop page', 'id' => 'page-' . $pagenumber));
478         }
480         return $output;
481     }
483     /**
484      * Returns the add menu that is output once per page.
485      * @param structure $structure object containing the structure of the quiz.
486      * @param int $page the page number that this menu will add to.
487      * @param \moodle_url $pageurl the canonical URL of this page.
488      * @param \question_edit_contexts $contexts the relevant question bank contexts.
489      * @param array $pagevars the variables from {@link \question_edit_setup()}.
490      * @return string HTML to output.
491      */
492     public function add_menu_actions(structure $structure, $page, \moodle_url $pageurl,
493             \question_edit_contexts $contexts, array $pagevars) {
495         $actions = $this->edit_menu_actions($structure, $page, $pageurl, $pagevars);
496         if (empty($actions)) {
497             return '';
498         }
499         $menu = new \action_menu();
500         $menu->set_alignment(\action_menu::TR, \action_menu::TR);
501         $menu->set_constraint('.mod-quiz-edit-content');
502         $trigger = html_writer::tag('span', get_string('add', 'quiz'), array('class' => 'add-menu'));
503         $menu->set_menu_trigger($trigger);
504         // The menu appears within an absolutely positioned element causing width problems.
505         // Make sure no-wrap is set so that we don't get a squashed menu.
506         $menu->set_nowrap_on_items(true);
508         // Disable the link if quiz has attempts.
509         if (!$structure->can_be_edited()) {
510             return $this->render($menu);
511         }
513         foreach ($actions as $action) {
514             if ($action instanceof \action_menu_link) {
515                 $action->add_class('add-menu');
516             }
517             $menu->add($action);
518         }
519         $menu->attributes['class'] .= ' page-add-actions commands';
521         // Prioritise the menu ahead of all other actions.
522         $menu->prioritise = true;
524         return $this->render($menu);
525     }
527     /**
528      * Returns the list of actions to go in the add menu.
529      * @param structure $structure object containing the structure of the quiz.
530      * @param int $page the page number that this menu will add to.
531      * @param \moodle_url $pageurl the canonical URL of this page.
532      * @param array $pagevars the variables from {@link \question_edit_setup()}.
533      * @return array the actions.
534      */
535     public function edit_menu_actions(structure $structure, $page,
536             \moodle_url $pageurl, array $pagevars) {
537         $questioncategoryid = question_get_category_id_from_pagevars($pagevars);
538         static $str;
539         if (!isset($str)) {
540             $str = get_strings(array('addasection', 'addaquestion', 'addarandomquestion',
541                     'addarandomselectedquestion', 'questionbank'), 'quiz');
542         }
544         // Get section, page, slotnumber and maxmark.
545         $actions = array();
547         // Add a new section to the add_menu if possible. This is always added to the HTML
548         // then hidden with CSS when no needed, so that as things are re-ordered, etc. with
549         // Ajax it can be relevaled again when necessary.
550         $params = array('cmid' => $structure->get_cmid(), 'addsectionatpage' => $page);
552         $actions['addasection'] = new \action_menu_link_secondary(
553                 new \moodle_url($pageurl, $params),
554                 new \pix_icon('t/add', $str->addasection, 'moodle', array('class' => 'iconsmall', 'title' => '')),
555                 $str->addasection, array('class' => 'cm-edit-action addasection', 'data-action' => 'addasection')
556         );
558         // Add a new question to the quiz.
559         $returnurl = new \moodle_url($pageurl, array('addonpage' => $page));
560         $params = array('returnurl' => $returnurl->out_as_local_url(false),
561                 'cmid' => $structure->get_cmid(), 'category' => $questioncategoryid,
562                 'addonpage' => $page, 'appendqnumstring' => 'addquestion');
564         $actions['addaquestion'] = new \action_menu_link_secondary(
565             new \moodle_url('/question/addquestion.php', $params),
566             new \pix_icon('t/add', $str->addaquestion, 'moodle', array('class' => 'iconsmall', 'title' => '')),
567             $str->addaquestion, array('class' => 'cm-edit-action addquestion', 'data-action' => 'addquestion')
568         );
570         // Call question bank.
571         $icon = new \pix_icon('t/add', $str->questionbank, 'moodle', array('class' => 'iconsmall', 'title' => ''));
572         if ($page) {
573             $title = get_string('addquestionfrombanktopage', 'quiz', $page);
574         } else {
575             $title = get_string('addquestionfrombankatend', 'quiz');
576         }
577         $attributes = array('class' => 'cm-edit-action questionbank',
578                 'data-header' => $title, 'data-action' => 'questionbank', 'data-addonpage' => $page);
579         $actions['questionbank'] = new \action_menu_link_secondary($pageurl, $icon, $str->questionbank, $attributes);
581         // Add a random question.
582         $returnurl = new \moodle_url('/mod/quiz/edit.php', array('cmid' => $structure->get_cmid(), 'data-addonpage' => $page));
583         $params = array('returnurl' => $returnurl, 'cmid' => $structure->get_cmid(), 'appendqnumstring' => 'addarandomquestion');
584         $url = new \moodle_url('/mod/quiz/addrandom.php', $params);
585         $icon = new \pix_icon('t/add', $str->addarandomquestion, 'moodle', array('class' => 'iconsmall', 'title' => ''));
586         $attributes = array('class' => 'cm-edit-action addarandomquestion', 'data-action' => 'addarandomquestion');
587         if ($page) {
588             $title = get_string('addrandomquestiontopage', 'quiz', $page);
589         } else {
590             $title = get_string('addrandomquestionatend', 'quiz');
591         }
592         $attributes = array_merge(array('data-header' => $title, 'data-addonpage' => $page), $attributes);
593         $actions['addarandomquestion'] = new \action_menu_link_secondary($url, $icon, $str->addarandomquestion, $attributes);
595         return $actions;
596     }
598     /**
599      * Render the form that contains the data for adding a new question to the quiz.
600      *
601      * @param structure $structure object containing the structure of the quiz.
602      * @param int $page the page number that this menu will add to.
603      * @param \moodle_url $pageurl the canonical URL of this page.
604      * @param array $pagevars the variables from {@link \question_edit_setup()}.
605      * @return string HTML to output.
606      */
607     protected function add_question_form(structure $structure, $page, \moodle_url $pageurl, array $pagevars) {
609         $questioncategoryid = question_get_category_id_from_pagevars($pagevars);
611         $output = html_writer::tag('input', null,
612                 array('type' => 'hidden', 'name' => 'returnurl',
613                         'value' => $pageurl->out_as_local_url(false, array('addonpage' => $page))));
614         $output .= html_writer::tag('input', null,
615                 array('type' => 'hidden', 'name' => 'cmid', 'value' => $structure->get_cmid()));
616         $output .= html_writer::tag('input', null,
617                 array('type' => 'hidden', 'name' => 'appendqnumstring', 'value' => 'addquestion'));
618         $output .= html_writer::tag('input', null,
619                 array('type' => 'hidden', 'name' => 'category', 'value' => $questioncategoryid));
621         return html_writer::tag('form', html_writer::div($output),
622                 array('class' => 'addnewquestion', 'method' => 'post',
623                         'action' => new \moodle_url('/question/addquestion.php')));
624     }
626     /**
627      * Display a question.
628      *
629      * @param structure $structure object containing the structure of the quiz.
630      * @param int $slot the first slot on the page we are outputting.
631      * @param \moodle_url $pageurl the canonical URL of this page.
632      * @return string HTML to output.
633      */
634     public function question(structure $structure, $slot, \moodle_url $pageurl) {
635         $output = '';
636         $output .= html_writer::start_tag('div');
638         if ($structure->can_be_edited()) {
639             $output .= $this->question_move_icon($structure, $slot);
640         }
642         $output .= html_writer::start_div('mod-indent-outer');
643         $output .= $this->question_number($structure->get_displayed_number_for_slot($slot));
645         // This div is used to indent the content.
646         $output .= html_writer::div('', 'mod-indent');
648         // Display the link to the question (or do nothing if question has no url).
649         if ($structure->get_question_type_for_slot($slot) == 'random') {
650             $questionname = $this->random_question($structure, $slot, $pageurl);
651         } else {
652             $questionname = $this->question_name($structure, $slot, $pageurl);
653         }
655         // Start the div for the activity title, excluding the edit icons.
656         $output .= html_writer::start_div('activityinstance');
657         $output .= $questionname;
659         // Closing the tag which contains everything but edit icons. Content part of the module should not be part of this.
660         $output .= html_writer::end_tag('div'); // .activityinstance.
662         // Action icons.
663         $questionicons = '';
664         $questionicons .= $this->question_preview_icon($structure->get_quiz(), $structure->get_question_in_slot($slot));
665         if ($structure->can_be_edited()) {
666             $questionicons .= $this->question_remove_icon($structure, $slot, $pageurl);
667         }
668         $questionicons .= $this->marked_out_of_field($structure, $slot);
669         $output .= html_writer::span($questionicons, 'actions'); // Required to add js spinner icon.
670         if ($structure->can_be_edited()) {
671             $output .= $this->question_dependency_icon($structure, $slot);
672         }
674         // End of indentation div.
675         $output .= html_writer::end_tag('div');
676         $output .= html_writer::end_tag('div');
678         return $output;
679     }
681     /**
682      * Render the move icon.
683      *
684      * @param structure $structure object containing the structure of the quiz.
685      * @param int $slot the first slot on the page we are outputting.
686      * @return string The markup for the move action.
687      */
688     public function question_move_icon(structure $structure, $slot) {
689         return html_writer::link(new \moodle_url('#'),
690             $this->pix_icon('i/dragdrop', get_string('move'), 'moodle', array('class' => 'iconsmall', 'title' => '')),
691             array('class' => 'editing_move', 'data-action' => 'move')
692         );
693     }
695     /**
696      * Output the question number.
697      * @param string $number The number, or 'i'.
698      * @return string HTML to output.
699      */
700     public function question_number($number) {
701         if (is_numeric($number)) {
702             $number = html_writer::span(get_string('question'), 'accesshide') . ' ' . $number;
703         }
704         return html_writer::tag('span', $number, array('class' => 'slotnumber'));
705     }
707     /**
708      * Render the preview icon.
709      *
710      * @param \stdClass $quiz the quiz settings from the database.
711      * @param \stdClass $question data from the question and quiz_slots tables.
712      * @param bool $label if true, show the preview question label after the icon
713      * @param int $variant which question variant to preview (optional).
714      * @return string HTML to output.
715      */
716     public function question_preview_icon($quiz, $question, $label = null, $variant = null) {
717         $url = quiz_question_preview_url($quiz, $question, $variant);
719         // Do we want a label?
720         $strpreviewlabel = '';
721         if ($label) {
722             $strpreviewlabel = ' ' . get_string('preview', 'quiz');
723         }
725         // Build the icon.
726         $strpreviewquestion = get_string('previewquestion', 'quiz');
727         $image = $this->pix_icon('t/preview', $strpreviewquestion);
729         $action = new \popup_action('click', $url, 'questionpreview',
730                                         question_preview_popup_params());
732         return $this->action_link($url, $image . $strpreviewlabel, $action,
733                 array('title' => $strpreviewquestion, 'class' => 'preview'));
734     }
736     /**
737      * Render an icon to remove a question from the quiz.
738      *
739      * @param structure $structure object containing the structure of the quiz.
740      * @param int $slot the first slot on the page we are outputting.
741      * @param \moodle_url $pageurl the canonical URL of the edit page.
742      * @return string HTML to output.
743      */
744     public function question_remove_icon(structure $structure, $slot, $pageurl) {
745         $url = new \moodle_url($pageurl, array('sesskey' => sesskey(), 'remove' => $slot));
746         $strdelete = get_string('delete');
748         $image = $this->pix_icon('t/delete', $strdelete);
750         return $this->action_link($url, $image, null, array('title' => $strdelete,
751                     'class' => 'cm-edit-action editing_delete', 'data-action' => 'delete'));
752     }
754     /**
755      * Display an icon to split or join two pages of the quiz.
756      *
757      * @param structure $structure object containing the structure of the quiz.
758      * @param int $slot the first slot on the page we are outputting.
759      * @return string HTML to output.
760      */
761     public function page_split_join_button($structure, $slot) {
762         $insertpagebreak = !$structure->is_last_slot_on_page($slot);
763         $url = new \moodle_url('repaginate.php', array('quizid' => $structure->get_quizid(),
764                 'slot' => $slot, 'repag' => $insertpagebreak ? 2 : 1, 'sesskey' => sesskey()));
766         if ($insertpagebreak) {
767             $title = get_string('addpagebreak', 'quiz');
768             $image = $this->image_icon('e/insert_page_break', $title);
769             $action = 'addpagebreak';
770         } else {
771             $title = get_string('removepagebreak', 'quiz');
772             $image = $this->image_icon('e/remove_page_break', $title);
773             $action = 'removepagebreak';
774         }
776         // Disable the link if quiz has attempts.
777         $disabled = null;
778         if (!$structure->can_be_edited()) {
779             $disabled = 'disabled';
780         }
781         return html_writer::span($this->action_link($url, $image, null, array('title' => $title,
782                     'class' => 'page_split_join cm-edit-action', 'disabled' => $disabled, 'data-action' => $action)),
783                 'page_split_join_wrapper');
784     }
786     /**
787      * Display the icon for whether this question can only be seen if the previous
788      * one has been answered.
789      *
790      * @param structure $structure object containing the structure of the quiz.
791      * @param int $slot the first slot on the page we are outputting.
792      * @return string HTML to output.
793      */
794     public function question_dependency_icon($structure, $slot) {
795         $a = array(
796             'thisq' => $structure->get_displayed_number_for_slot($slot),
797             'previousq' => $structure->get_displayed_number_for_slot(max($slot - 1, 1)),
798         );
799         if ($structure->is_question_dependent_on_previous_slot($slot)) {
800             $title = get_string('questiondependencyremove', 'quiz', $a);
801             $image = $this->pix_icon('t/locked', get_string('questiondependsonprevious', 'quiz'),
802                     'moodle', array('title' => ''));
803             $action = 'removedependency';
804         } else {
805             $title = get_string('questiondependencyadd', 'quiz', $a);
806             $image = $this->pix_icon('t/unlocked', get_string('questiondependencyfree', 'quiz'),
807                     'moodle', array('title' => ''));
808             $action = 'adddependency';
809         }
811         // Disable the link if quiz has attempts.
812         $disabled = null;
813         if (!$structure->can_be_edited()) {
814             $disabled = 'disabled';
815         }
816         $extraclass = '';
817         if (!$structure->can_question_depend_on_previous_slot($slot)) {
818             $extraclass = ' question_dependency_cannot_depend';
819         }
820         return html_writer::span($this->action_link('#', $image, null, array('title' => $title,
821                 'class' => 'cm-edit-action', 'disabled' => $disabled, 'data-action' => $action)),
822                 'question_dependency_wrapper' . $extraclass);
823     }
825     /**
826      * Renders html to display a name with the link to the question on a quiz edit page
827      *
828      * If the user does not have permission to edi the question, it is rendered
829      * without a link
830      *
831      * @param structure $structure object containing the structure of the quiz.
832      * @param int $slot which slot we are outputting.
833      * @param \moodle_url $pageurl the canonical URL of this page.
834      * @return string HTML to output.
835      */
836     public function question_name(structure $structure, $slot, $pageurl) {
837         $output = '';
839         $question = $structure->get_question_in_slot($slot);
840         $editurl = new \moodle_url('/question/question.php', array(
841                 'returnurl' => $pageurl->out_as_local_url(),
842                 'cmid' => $structure->get_cmid(), 'id' => $question->id));
844         $instancename = quiz_question_tostring($question);
846         $qtype = \question_bank::get_qtype($question->qtype, false);
847         $namestr = $qtype->local_name();
849         $icon = $this->pix_icon('icon', $namestr, $qtype->plugin_name(), array('title' => $namestr,
850                 'class' => 'icon activityicon', 'alt' => ' ', 'role' => 'presentation'));
852         $editicon = $this->pix_icon('t/edit', '', 'moodle', array('title' => ''));
854         // Need plain question name without html tags for link title.
855         $title = shorten_text(format_string($question->name), 100);
857         // Display the link itself.
858         $activitylink = $icon . html_writer::tag('span', $editicon . $instancename, array('class' => 'instancename'));
859         $output .= html_writer::link($editurl, $activitylink,
860                 array('title' => get_string('editquestion', 'quiz').' '.$title));
862         return $output;
863     }
865     /**
866      * Renders html to display a random question the link to edit the configuration
867      * and also to see that category in the question bank.
868      *
869      * @param structure $structure object containing the structure of the quiz.
870      * @param int $slot which slot we are outputting.
871      * @param \moodle_url $pageurl the canonical URL of this page.
872      * @return string HTML to output.
873      */
874     public function random_question(structure $structure, $slot, $pageurl) {
876         $question = $structure->get_question_in_slot($slot);
877         $editurl = new \moodle_url('/question/question.php', array(
878                 'returnurl' => $pageurl->out_as_local_url(),
879                 'cmid' => $structure->get_cmid(), 'id' => $question->id));
881         $temp = clone($question);
882         $temp->questiontext = '';
883         $instancename = quiz_question_tostring($temp);
885         $configuretitle = get_string('configurerandomquestion', 'quiz');
886         $qtype = \question_bank::get_qtype($question->qtype, false);
887         $namestr = $qtype->local_name();
888         $icon = $this->pix_icon('icon', $namestr, $qtype->plugin_name(), array('title' => $namestr,
889                 'class' => 'icon activityicon', 'alt' => ' ', 'role' => 'presentation'));
891         $editicon = $this->pix_icon('t/edit', $configuretitle, 'moodle', array('title' => ''));
893         // If this is a random question, display a link to show the questions
894         // selected from in the question bank.
895         $qbankurl = new \moodle_url('/question/edit.php', array(
896                 'cmid' => $structure->get_cmid(),
897                 'cat' => $question->category . ',' . $question->contextid,
898                 'recurse' => !empty($question->questiontext)));
899         $qbanklink = ' ' . \html_writer::link($qbankurl,
900                 get_string('seequestions', 'quiz'), array('class' => 'mod_quiz_random_qbank_link'));
902         return html_writer::link($editurl, $icon . $editicon, array('title' => $configuretitle)) .
903                 ' ' . $instancename . ' ' . $qbanklink;
904     }
906     /**
907      * Display the 'marked out of' information for a question.
908      * Along with the regrade action.
909      * @param structure $structure object containing the structure of the quiz.
910      * @param int $slot which slot we are outputting.
911      * @return string HTML to output.
912      */
913     public function marked_out_of_field(structure $structure, $slot) {
914         if (!$structure->is_real_question($slot)) {
915             $output = html_writer::span('',
916                     'instancemaxmark decimalplaces_' . $structure->get_decimal_places_for_question_marks());
918             $output .= html_writer::span(
919                     $this->pix_icon('spacer', '', 'moodle', array('class' => 'editicon visibleifjs', 'title' => '')),
920                     'editing_maxmark');
921             return html_writer::span($output, 'instancemaxmarkcontainer infoitem');
922         }
924         $output = html_writer::span($structure->formatted_question_grade($slot),
925                 'instancemaxmark decimalplaces_' . $structure->get_decimal_places_for_question_marks(),
926                 array('title' => get_string('maxmark', 'quiz')));
928         $output .= html_writer::span(
929             html_writer::link(
930                 new \moodle_url('#'),
931                 $this->pix_icon('t/editstring', '', 'moodle', array('class' => 'editicon visibleifjs', 'title' => '')),
932                 array(
933                     'class' => 'editing_maxmark',
934                     'data-action' => 'editmaxmark',
935                     'title' => get_string('editmaxmark', 'quiz'),
936                 )
937             )
938         );
939         return html_writer::span($output, 'instancemaxmarkcontainer');
940     }
942     /**
943      * Renders the question chooser.
944      *
945      * @param renderable
946      * @return string
947      */
948     public function render_question_chooser(renderable $chooser) {
949         return $this->render_from_template('mod_quiz/question_chooser', $chooser->export_for_template($this));
950     }
952     /**
953      * Render the question type chooser dialogue.
954      * @return string HTML to output.
955      */
956     public function question_chooser() {
957         $chooser = \mod_quiz\output\question_chooser::get($this->page->course, [], null);
958         $container = html_writer::div($this->render($chooser), '', array('id' => 'qtypechoicecontainer'));
959         return html_writer::div($container, 'createnewquestion');
960     }
962     /**
963      * Render the contents of the question bank pop-up in its initial state,
964      * when it just contains a loading progress indicator.
965      * @return string HTML to output.
966      */
967     public function question_bank_loading() {
968         return html_writer::div($this->pix_icon('i/loading', get_string('loading')), 'questionbankloading');
969     }
971     /**
972      * Return random question form.
973      * @param \moodle_url $thispageurl the canonical URL of this page.
974      * @param \question_edit_contexts $contexts the relevant question bank contexts.
975      * @param array $pagevars the variables from {@link \question_edit_setup()}.
976      * @return string HTML to output.
977      */
978     protected function random_question_form(\moodle_url $thispageurl, \question_edit_contexts $contexts, array $pagevars) {
980         if (!$contexts->have_cap('moodle/question:useall')) {
981             return '';
982         }
983         $randomform = new \quiz_add_random_form(new \moodle_url('/mod/quiz/addrandom.php'),
984                                  array('contexts' => $contexts, 'cat' => $pagevars['cat']));
985         $randomform->set_data(array(
986                 'category' => $pagevars['cat'],
987                 'returnurl' => $thispageurl->out_as_local_url(true),
988                 'randomnumber' => 1,
989                 'cmid' => $thispageurl->param('cmid'),
990         ));
991         return html_writer::div($randomform->render(), 'randomquestionformforpopup');
992     }
994     /**
995      * Initialise the JavaScript for the general editing. (JavaScript for popups
996      * is handled with the specific code for those.)
997      *
998      * @param structure $structure object containing the structure of the quiz.
999      * @param \question_edit_contexts $contexts the relevant question bank contexts.
1000      * @param array $pagevars the variables from {@link \question_edit_setup()}.
1001      * @param \moodle_url $pageurl the canonical URL of this page.
1002      * @return bool Always returns true
1003      */
1004     protected function initialise_editing_javascript(structure $structure,
1005             \question_edit_contexts $contexts, array $pagevars, \moodle_url $pageurl) {
1007         $config = new \stdClass();
1008         $config->resourceurl = '/mod/quiz/edit_rest.php';
1009         $config->sectionurl = '/mod/quiz/edit_rest.php';
1010         $config->pageparams = array();
1011         $config->questiondecimalpoints = $structure->get_decimal_places_for_question_marks();
1012         $config->pagehtml = $this->new_page_template($structure, $contexts, $pagevars, $pageurl);
1013         $config->addpageiconhtml = $this->add_page_icon_template($structure);
1015         $this->page->requires->yui_module('moodle-mod_quiz-toolboxes',
1016                 'M.mod_quiz.init_resource_toolbox',
1017                 array(array(
1018                         'courseid' => $structure->get_courseid(),
1019                         'quizid' => $structure->get_quizid(),
1020                         'ajaxurl' => $config->resourceurl,
1021                         'config' => $config,
1022                 ))
1023         );
1024         unset($config->pagehtml);
1025         unset($config->addpageiconhtml);
1027         $this->page->requires->yui_module('moodle-mod_quiz-toolboxes',
1028                 'M.mod_quiz.init_section_toolbox',
1029                 array(array(
1030                         'courseid' => $structure,
1031                         'quizid' => $structure->get_quizid(),
1032                         'ajaxurl' => $config->sectionurl,
1033                         'config' => $config,
1034                 ))
1035         );
1037         $this->page->requires->yui_module('moodle-mod_quiz-dragdrop', 'M.mod_quiz.init_section_dragdrop',
1038                 array(array(
1039                         'courseid' => $structure,
1040                         'quizid' => $structure->get_quizid(),
1041                         'ajaxurl' => $config->sectionurl,
1042                         'config' => $config,
1043                 )), null, true);
1045         $this->page->requires->yui_module('moodle-mod_quiz-dragdrop', 'M.mod_quiz.init_resource_dragdrop',
1046                 array(array(
1047                         'courseid' => $structure,
1048                         'quizid' => $structure->get_quizid(),
1049                         'ajaxurl' => $config->resourceurl,
1050                         'config' => $config,
1051                 )), null, true);
1053         // Require various strings for the command toolbox.
1054         $this->page->requires->strings_for_js(array(
1055                 'clicktohideshow',
1056                 'deletechecktype',
1057                 'deletechecktypename',
1058                 'edittitle',
1059                 'edittitleinstructions',
1060                 'emptydragdropregion',
1061                 'hide',
1062                 'markedthistopic',
1063                 'markthistopic',
1064                 'move',
1065                 'movecontent',
1066                 'moveleft',
1067                 'movesection',
1068                 'page',
1069                 'question',
1070                 'selectall',
1071                 'show',
1072                 'tocontent',
1073         ), 'moodle');
1075         $this->page->requires->strings_for_js(array(
1076                 'addpagebreak',
1077                 'confirmremovesectionheading',
1078                 'confirmremovequestion',
1079                 'dragtoafter',
1080                 'dragtostart',
1081                 'numquestionsx',
1082                 'sectionheadingedit',
1083                 'sectionheadingremove',
1084                 'removepagebreak',
1085                 'questiondependencyadd',
1086                 'questiondependencyfree',
1087                 'questiondependencyremove',
1088                 'questiondependsonprevious',
1089         ), 'quiz');
1091         foreach (\question_bank::get_all_qtypes() as $qtype => $notused) {
1092             $this->page->requires->string_for_js('pluginname', 'qtype_' . $qtype);
1093         }
1095         return true;
1096     }
1098     /**
1099      * HTML for a page, with ids stripped, so it can be used as a javascript template.
1100      *
1101      * @param structure $structure object containing the structure of the quiz.
1102      * @param \question_edit_contexts $contexts the relevant question bank contexts.
1103      * @param array $pagevars the variables from {@link \question_edit_setup()}.
1104      * @param \moodle_url $pageurl the canonical URL of this page.
1105      * @return string HTML for a new page.
1106      */
1107     protected function new_page_template(structure $structure,
1108             \question_edit_contexts $contexts, array $pagevars, \moodle_url $pageurl) {
1109         if (!$structure->has_questions()) {
1110             return '';
1111         }
1113         $pagehtml = $this->page_row($structure, 1, $contexts, $pagevars, $pageurl);
1115         // Normalise the page number.
1116         $pagenumber = $structure->get_page_number_for_slot(1);
1117         $strcontexts = array();
1118         $strcontexts[] = 'page-';
1119         $strcontexts[] = get_string('page') . ' ';
1120         $strcontexts[] = 'addonpage%3D';
1121         $strcontexts[] = 'addonpage=';
1122         $strcontexts[] = 'addonpage="';
1123         $strcontexts[] = get_string('addquestionfrombanktopage', 'quiz', '');
1124         $strcontexts[] = 'data-addonpage%3D';
1125         $strcontexts[] = 'action-menu-';
1127         foreach ($strcontexts as $strcontext) {
1128             $pagehtml = str_replace($strcontext . $pagenumber, $strcontext . '%%PAGENUMBER%%', $pagehtml);
1129         }
1131         return $pagehtml;
1132     }
1134     /**
1135      * HTML for a page, with ids stripped, so it can be used as a javascript template.
1136      *
1137      * @param structure $structure object containing the structure of the quiz.
1138      * @return string HTML for a new icon
1139      */
1140     protected function add_page_icon_template(structure $structure) {
1142         if (!$structure->has_questions()) {
1143             return '';
1144         }
1146         $html = $this->page_split_join_button($structure, 1);
1147         return str_replace('&amp;slot=1&amp;', '&amp;slot=%%SLOT%%&amp;', $html);
1148     }
1150     /**
1151      * Return the contents of the question bank, to be displayed in the question-bank pop-up.
1152      *
1153      * @param \mod_quiz\question\bank\custom_view $questionbank the question bank view object.
1154      * @param array $pagevars the variables from {@link \question_edit_setup()}.
1155      * @return string HTML to output / send back in response to an AJAX request.
1156      */
1157     public function question_bank_contents(\mod_quiz\question\bank\custom_view $questionbank, array $pagevars) {
1159         $qbank = $questionbank->render('editq', $pagevars['qpage'], $pagevars['qperpage'],
1160                 $pagevars['cat'], $pagevars['recurse'], $pagevars['showhidden'], $pagevars['qbshowtext']);
1161         return html_writer::div(html_writer::div($qbank, 'bd'), 'questionbankformforpopup');
1162     }