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/>.
16
17/**
18 * Contains class mod_feedback_complete_form
19 *
20 * @package   mod_feedback
21 * @copyright 2016 Marina Glancy
22 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23 */
24
25defined('MOODLE_INTERNAL') || die();
26
27/**
28 * Class mod_feedback_complete_form
29 *
30 * @package   mod_feedback
31 * @copyright 2016 Marina Glancy
32 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
33 */
34class mod_feedback_complete_form extends moodleform {
35
36    /** @var int */
37    const MODE_COMPLETE = 1;
38    /** @var int */
39    const MODE_PRINT = 2;
40    /** @var int */
41    const MODE_EDIT = 3;
42    /** @var int */
43    const MODE_VIEW_RESPONSE = 4;
44    /** @var int */
45    const MODE_VIEW_TEMPLATE = 5;
46
47    /** @var int */
48    protected $mode;
49    /** @var mod_feedback_structure|mod_feedback_completion */
50    protected $structure;
51    /** @var mod_feedback_completion */
52    protected $completion;
53    /** @var int */
54    protected $gopage;
55    /** @var bool */
56    protected $hasrequired = false;
57
58    /**
59     * Constructor
60     *
61     * @param int $mode
62     * @param mod_feedback_structure $structure
63     * @param string $formid CSS id attribute of the form
64     * @param array $customdata
65     */
66    public function __construct($mode, mod_feedback_structure $structure, $formid, $customdata = null) {
67        $this->mode = $mode;
68        $this->structure = $structure;
69        $this->gopage = isset($customdata['gopage']) ? $customdata['gopage'] : 0;
70        $isanonymous = $this->structure->is_anonymous() ? ' ianonymous' : '';
71        parent::__construct(null, $customdata, 'POST', '',
72                array('id' => $formid, 'class' => 'feedback_form' . $isanonymous), true);
73        $this->set_display_vertical();
74    }
75
76    /**
77     * Form definition
78     */
79    public function definition() {
80        $mform = $this->_form;
81        $mform->addElement('hidden', 'id', $this->get_cm()->id);
82        $mform->setType('id', PARAM_INT);
83        $mform->addElement('hidden', 'courseid', $this->get_current_course_id());
84        $mform->setType('courseid', PARAM_INT);
85        $mform->addElement('hidden', 'gopage');
86        $mform->setType('gopage', PARAM_INT);
87        $mform->addElement('hidden', 'lastpage');
88        $mform->setType('lastpage', PARAM_INT);
89        $mform->addElement('hidden', 'startitempos');
90        $mform->setType('startitempos', PARAM_INT);
91        $mform->addElement('hidden', 'lastitempos');
92        $mform->setType('lastitempos', PARAM_INT);
93
94        if (isloggedin() && !isguestuser() && $this->mode != self::MODE_EDIT && $this->mode != self::MODE_VIEW_TEMPLATE &&
95                    $this->mode != self::MODE_VIEW_RESPONSE) {
96            // Output information about the current mode (anonymous or not) in some modes.
97            if ($this->structure->is_anonymous()) {
98                $anonymousmodeinfo = get_string('anonymous', 'feedback');
99            } else {
100                $anonymousmodeinfo = get_string('non_anonymous', 'feedback');
101            }
102            $element = $mform->addElement('static', 'anonymousmode', '',
103                    get_string('mode', 'feedback') . ': ' . $anonymousmodeinfo);
104            $element->setAttributes($element->getAttributes() + ['class' => 'feedback_mode']);
105        }
106
107        // Add buttons to go to previous/next pages and submit the feedback.
108        if ($this->mode == self::MODE_COMPLETE) {
109            $buttonarray = array();
110            $buttonarray[] = &$mform->createElement('submit', 'gopreviouspage', get_string('previous_page', 'feedback'));
111            $buttonarray[] = &$mform->createElement('submit', 'gonextpage', get_string('next_page', 'feedback'),
112                    array('class' => 'form-submit'));
113            $buttonarray[] = &$mform->createElement('submit', 'savevalues', get_string('save_entries', 'feedback'),
114                    array('class' => 'form-submit'));
115            $buttonarray[] = &$mform->createElement('cancel');
116            $mform->addGroup($buttonarray, 'buttonar', '', array(' '), false);
117            $mform->closeHeaderBefore('buttonar');
118        }
119
120        if ($this->mode == self::MODE_COMPLETE) {
121            $this->definition_complete();
122        } else {
123            $this->definition_preview();
124        }
125
126        // Set data.
127        $this->set_data(array('gopage' => $this->gopage));
128    }
129
130    /**
131     * Called from definition_after_data() in the completion mode
132     *
133     * This will add only items from a current page to the feedback and adjust the buttons
134     */
135    protected function definition_complete() {
136        if (!$this->structure instanceof mod_feedback_completion) {
137            // We should not really be here but just in case.
138            return;
139        }
140        $pages = $this->structure->get_pages();
141        $gopage = $this->gopage;
142        $pageitems = $pages[$gopage];
143        $hasnextpage = $gopage < count($pages) - 1; // Until we complete this page we can not trust get_next_page().
144        $hasprevpage = $gopage && ($this->structure->get_previous_page($gopage, false) !== null);
145
146        // Add elements.
147        foreach ($pageitems as $item) {
148            $itemobj = feedback_get_item_class($item->typ);
149            $itemobj->complete_form_element($item, $this);
150        }
151
152        // Remove invalid buttons (for example, no "previous page" if we are on the first page).
153        if (!$hasprevpage) {
154            $this->remove_button('gopreviouspage');
155        }
156        if (!$hasnextpage) {
157            $this->remove_button('gonextpage');
158        }
159        if ($hasnextpage) {
160            $this->remove_button('savevalues');
161        }
162    }
163
164    /**
165     * Called from definition_after_data() in all modes except for completion
166     *
167     * This will add all items to the form, including pagebreaks as horizontal rules.
168     */
169    protected function definition_preview() {
170        foreach ($this->structure->get_items() as $feedbackitem) {
171            $itemobj = feedback_get_item_class($feedbackitem->typ);
172            $itemobj->complete_form_element($feedbackitem, $this);
173        }
174    }
175
176    /**
177     * Removes the button that is not applicable for the current page
178     *
179     * @param string $buttonname
180     */
181    private function remove_button($buttonname) {
182        $el = $this->_form->getElement('buttonar');
183        foreach ($el->_elements as $idx => $button) {
184            if ($button instanceof MoodleQuickForm_submit && $button->getName() === $buttonname) {
185                unset($el->_elements[$idx]);
186                return;
187            }
188        }
189    }
190
191    /**
192     * Returns value for this element that is already stored in temporary or permanent table,
193     * usually only available when user clicked "Previous page". Null means no value is stored.
194     *
195     * @param stdClass $item
196     * @return string
197     */
198    public function get_item_value($item) {
199        if ($this->structure instanceof mod_feedback_completion) {
200            return $this->structure->get_item_value($item);
201        }
202        return null;
203    }
204
205    /**
206     * Can be used by the items to get the course id for which feedback is taken
207     *
208     * This function returns 0 for feedbacks that are located inside the courses.
209     * $this->get_feedback()->course will return the course where feedback is located.
210     * $this->get_current_course_id() will return the course where user was before taking the feedback
211     *
212     * @return int
213     */
214    public function get_course_id() {
215        return $this->structure->get_courseid();
216    }
217
218    /**
219     * Record from 'feedback' table corresponding to the current feedback
220     * @return stdClass
221     */
222    public function get_feedback() {
223        return $this->structure->get_feedback();
224    }
225
226    /**
227     * Current feedback mode, see constants on the top of this class
228     * @return int
229     */
230    public function get_mode() {
231        return $this->mode;
232    }
233
234    /**
235     * Returns whether the form is frozen, some items may prefer to change the element
236     * type in case of frozen form. For example, text or textarea element does not look
237     * nice when frozen
238     *
239     * @return bool
240     */
241    public function is_frozen() {
242        return $this->mode == self::MODE_VIEW_RESPONSE;
243    }
244
245    /**
246     * Returns the current course module
247     * @return cm_info
248     */
249    public function get_cm() {
250        return $this->structure->get_cm();
251    }
252
253    /**
254     * Returns the course where user was before taking the feedback.
255     *
256     * For feedbacks inside the course it will be the same as $this->get_feedback()->course.
257     * For feedbacks on the frontpage it will be the same as $this->get_course_id()
258     *
259     * @return int
260     */
261    public function get_current_course_id() {
262        return $this->structure->get_courseid() ?: $this->get_feedback()->course;
263    }
264
265    /**
266     * CSS class for the item
267     * @param stdClass $item
268     * @return string
269     */
270    protected function get_suggested_class($item) {
271        $class = "feedback_itemlist feedback-item-{$item->typ}";
272        if ($item->dependitem) {
273            $class .= " feedback_is_dependent";
274        }
275        if ($item->typ !== 'pagebreak') {
276            $itemobj = feedback_get_item_class($item->typ);
277            if ($itemobj->get_hasvalue()) {
278                $class .= " feedback_hasvalue";
279            }
280        }
281        return $class;
282    }
283
284    /**
285     * Adds an element to this form - to be used by items in their complete_form_element() method
286     *
287     * @param stdClass $item
288     * @param HTML_QuickForm_element|array $element either completed form element or an array that
289     *      can be passed as arguments to $this->_form->createElement() function
290     * @param bool $addrequiredrule automatically add 'required' rule
291     * @param bool $setdefaultvalue automatically set default value for element
292     * @return HTML_QuickForm_element
293     */
294    public function add_form_element($item, $element, $addrequiredrule = true, $setdefaultvalue = true) {
295        global $OUTPUT;
296
297        if (is_array($element) && $element[0] == 'group') {
298            // For groups, use the mforms addGroup API.
299            // $element looks like: ['group', $groupinputname, $name, $objects, $separator, $appendname],
300            $element = $this->_form->addGroup($element[3], $element[1], $element[2], $element[4], $element[5]);
301        } else {
302            // Add non-group element to the form.
303            if (is_array($element)) {
304                if ($this->is_frozen() && $element[0] === 'text') {
305                    // Convert 'text' element to 'static' when freezing for better display.
306                    $element = ['static', $element[1], $element[2]];
307                }
308                $element = call_user_func_array(array($this->_form, 'createElement'), $element);
309            }
310            $element = $this->_form->addElement($element);
311        }
312
313        // Prepend standard CSS classes to the element classes.
314        $attributes = $element->getAttributes();
315        $class = !empty($attributes['class']) ? ' ' . $attributes['class'] : '';
316        $attributes['class'] = $this->get_suggested_class($item) . $class;
317        $element->setAttributes($attributes);
318
319        // Add required rule.
320        if ($item->required && $addrequiredrule) {
321            $this->_form->addRule($element->getName(), get_string('required'), 'required', null, 'client');
322        }
323
324        // Set default value.
325        if ($setdefaultvalue && ($tmpvalue = $this->get_item_value($item))) {
326            $this->_form->setDefault($element->getName(), s($tmpvalue));
327        }
328
329        // Freeze if needed.
330        if ($this->is_frozen()) {
331            $element->freeze();
332        }
333
334        // Add red asterisks on required fields.
335        if ($item->required) {
336            $required = $OUTPUT->pix_icon('req', get_string('requiredelement', 'form'));
337            $element->setLabel($element->getLabel() . $required);
338            $this->hasrequired = true;
339        }
340
341        // Add different useful stuff to the question name.
342        $this->add_item_label($item, $element);
343        $this->add_item_dependencies($item, $element);
344        $this->add_item_number($item, $element);
345
346        if ($this->mode == self::MODE_EDIT) {
347            $this->enhance_name_for_edit($item, $element);
348        }
349
350        return $element;
351    }
352
353    /**
354     * Adds a group element to this form - to be used by items in their complete_form_element() method
355     *
356     * @param stdClass $item
357     * @param string $groupinputname name for the form element
358     * @param string $name question text
359     * @param array $elements array of arrays that can be passed to $this->_form->createElement()
360     * @param string $separator separator between group elements
361     * @param string $class additional CSS classes for the form element
362     * @return HTML_QuickForm_element
363     */
364    public function add_form_group_element($item, $groupinputname, $name, $elements, $separator,
365            $class = '') {
366        $objects = array();
367        foreach ($elements as $element) {
368            $object = call_user_func_array(array($this->_form, 'createElement'), $element);
369            $objects[] = $object;
370        }
371        $element = $this->add_form_element($item,
372                ['group', $groupinputname, $name, $objects, $separator, false],
373                false,
374                false);
375        if ($class !== '') {
376            $attributes = $element->getAttributes();
377            $attributes['class'] .= ' ' . $class;
378            $element->setAttributes($attributes);
379        }
380        return $element;
381    }
382
383    /**
384     * Adds an item number to the question name (if feedback autonumbering is on)
385     * @param stdClass $item
386     * @param HTML_QuickForm_element $element
387     */
388    protected function add_item_number($item, $element) {
389        if ($this->get_feedback()->autonumbering && !empty($item->itemnr)) {
390            $name = $element->getLabel();
391            $element->setLabel(html_writer::span($item->itemnr. '.', 'itemnr') . ' ' . $name);
392        }
393    }
394
395    /**
396     * Adds an item label to the question name
397     * @param stdClass $item
398     * @param HTML_QuickForm_element $element
399     */
400    protected function add_item_label($item, $element) {
401        if (strlen($item->label) && ($this->mode == self::MODE_EDIT || $this->mode == self::MODE_VIEW_TEMPLATE)) {
402            $name = get_string('nameandlabelformat', 'mod_feedback',
403                (object)['label' => format_string($item->label), 'name' => $element->getLabel()]);
404            $element->setLabel($name);
405        }
406    }
407
408    /**
409     * Adds a dependency description to the question name
410     * @param stdClass $item
411     * @param HTML_QuickForm_element $element
412     */
413    protected function add_item_dependencies($item, $element) {
414        $allitems = $this->structure->get_items();
415        if ($item->dependitem && ($this->mode == self::MODE_EDIT || $this->mode == self::MODE_VIEW_TEMPLATE)) {
416            if (isset($allitems[$item->dependitem])) {
417                $dependitem = $allitems[$item->dependitem];
418                $name = $element->getLabel();
419                $name .= html_writer::span(' ('.format_string($dependitem->label).'-&gt;'.$item->dependvalue.')',
420                        'feedback_depend');
421                $element->setLabel($name);
422            }
423        }
424    }
425
426    /**
427     * Returns the CSS id attribute that will be assigned by moodleform later to this element
428     * @param stdClass $item
429     * @param HTML_QuickForm_element $element
430     */
431    protected function guess_element_id($item, $element) {
432        if (!$id = $element->getAttribute('id')) {
433            $attributes = $element->getAttributes();
434            $id = $attributes['id'] = 'feedback_item_' . $item->id;
435            $element->setAttributes($attributes);
436        }
437        if ($element->getType() === 'group') {
438            return 'fgroup_' . $id;
439        }
440        return 'fitem_' . $id;
441    }
442
443    /**
444     * Adds editing actions to the question name in the edit mode
445     * @param stdClass $item
446     * @param HTML_QuickForm_element $element
447     */
448    protected function enhance_name_for_edit($item, $element) {
449        global $OUTPUT;
450        $menu = new action_menu();
451        $menu->set_owner_selector('#' . $this->guess_element_id($item, $element));
452        $menu->set_constraint('.feedback_form');
453        $menu->set_alignment(action_menu::TR, action_menu::BR);
454        $menu->set_menu_trigger(get_string('edit'));
455        $menu->prioritise = true;
456
457        $itemobj = feedback_get_item_class($item->typ);
458        $actions = $itemobj->edit_actions($item, $this->get_feedback(), $this->get_cm());
459        foreach ($actions as $action) {
460            $menu->add($action);
461        }
462        $editmenu = $OUTPUT->render($menu);
463
464        $name = $element->getLabel();
465
466        $name = html_writer::span('', 'itemdd', array('id' => 'feedback_item_box_' . $item->id)) .
467                html_writer::span($name, 'itemname') .
468                html_writer::span($editmenu, 'itemactions');
469        $element->setLabel(html_writer::span($name, 'itemtitle'));
470    }
471
472    /**
473     * Sets the default value for form element - alias to $this->_form->setDefault()
474     * @param HTML_QuickForm_element|string $element
475     * @param mixed $defaultvalue
476     */
477    public function set_element_default($element, $defaultvalue) {
478        if ($element instanceof HTML_QuickForm_element) {
479            $element = $element->getName();
480        }
481        $this->_form->setDefault($element, $defaultvalue);
482    }
483
484
485    /**
486     * Sets the default value for form element - wrapper to $this->_form->setType()
487     * @param HTML_QuickForm_element|string $element
488     * @param int $type
489     */
490    public function set_element_type($element, $type) {
491        if ($element instanceof HTML_QuickForm_element) {
492            $element = $element->getName();
493        }
494        $this->_form->setType($element, $type);
495    }
496
497    /**
498     * Adds a validation rule for the given field - wrapper for $this->_form->addRule()
499     *
500     * Do not use for 'required' rule!
501     * Required * will be added automatically, if additional validation is needed
502     * use method {@link self::add_validation_rule()}
503     *
504     * @param string $element Form element name
505     * @param string $message Message to display for invalid data
506     * @param string $type Rule type, use getRegisteredRules() to get types
507     * @param string $format (optional)Required for extra rule data
508     * @param string $validation (optional)Where to perform validation: "server", "client"
509     * @param bool $reset Client-side validation: reset the form element to its original value if there is an error?
510     * @param bool $force Force the rule to be applied, even if the target form element does not exist
511     */
512    public function add_element_rule($element, $message, $type, $format = null, $validation = 'server',
513            $reset = false, $force = false) {
514        if ($element instanceof HTML_QuickForm_element) {
515            $element = $element->getName();
516        }
517        $this->_form->addRule($element, $message, $type, $format, $validation, $reset, $force);
518    }
519
520    /**
521     * Adds a validation rule to the form
522     *
523     * @param callable $callback with arguments ($values, $files)
524     */
525    public function add_validation_rule(callable $callback) {
526        if ($this->mode == self::MODE_COMPLETE) {
527            $this->_form->addFormRule($callback);
528        }
529    }
530
531    /**
532     * Returns a reference to the element - wrapper for function $this->_form->getElement()
533     *
534     * @param string $elementname Element name
535     * @return HTML_QuickForm_element reference to element
536     */
537    public function get_form_element($elementname) {
538        return $this->_form->getElement($elementname);
539    }
540
541    /**
542     * Displays the form
543     */
544    public function display() {
545        global $OUTPUT, $PAGE;
546        // Finalize the form definition if not yet done.
547        if (!$this->_definition_finalized) {
548            $this->_definition_finalized = true;
549            $this->definition_after_data();
550        }
551
552        $mform = $this->_form;
553
554        // Add "This form has required fields" text in the bottom of the form.
555        if (($mform->_required || $this->hasrequired) &&
556               ($this->mode == self::MODE_COMPLETE || $this->mode == self::MODE_PRINT || $this->mode == self::MODE_VIEW_TEMPLATE)) {
557            $element = $mform->addElement('static', 'requiredfields', '',
558                    get_string('somefieldsrequired', 'form',
559                            $OUTPUT->pix_icon('req', get_string('requiredelement', 'form'))));
560            $element->setAttributes($element->getAttributes() + ['class' => 'requirednote']);
561        }
562
563        // Reset _required array so the default red * are not displayed.
564        $mform->_required = array();
565
566        // Move buttons to the end of the form.
567        if ($this->mode == self::MODE_COMPLETE) {
568            $mform->addElement('hidden', '__dummyelement');
569            $buttons = $mform->removeElement('buttonar', false);
570            $mform->insertElementBefore($buttons, '__dummyelement');
571            $mform->removeElement('__dummyelement');
572        }
573
574        $this->_form->display();
575
576        if ($this->mode == self::MODE_EDIT) {
577            $PAGE->requires->js_call_amd('mod_feedback/edit', 'setup');
578        }
579    }
580}
581