Merge branch 'MDL-64155-master' of git://github.com/vmdef/moodle
[moodle.git] / question / type / gapselect / edit_form_base.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  * Base class for editing question types like this one.
19  *
20  * @package    qtype_gapselect
21  * @copyright  2011 The Open University
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 defined('MOODLE_INTERNAL') || die();
28 /**
29  * Elements embedded in question text editing form definition.
30  *
31  * @copyright  2011 The Open University
32  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
33  */
34 class qtype_gapselect_edit_form_base extends question_edit_form {
36     /** @var array of HTML tags allowed in choices / drag boxes. */
37     protected $allowedhtmltags = array(
38         'sub',
39         'sup',
40         'b',
41         'i',
42         'em',
43         'strong'
44     );
46     /** @var string regex to match HTML open tags. */
47     private $htmltstarttagsandattributes = '~<\s*\w+\b[^>]*>~';
49     /** @var string regex to match HTML close tags or br. */
50     private $htmltclosetags = '~<\s*/\s*\w+\b[^>]*>~';
52     /** @var string regex to select text like [[cat]] (including the square brackets). */
53     private $squarebracketsregex = '/\[\[[^]]*?\]\]/';
55     /**
56      * Vaidate some input to make sure it does not contain any tags other than
57      * $this->allowedhtmltags.
58      * @param unknown_type $text the input to validate.
59      * @return string any validation errors.
60      */
61     protected function get_illegal_tag_error($text) {
62         // Remove legal tags.
63         $strippedtext = $text;
64         foreach ($this->allowedhtmltags as $htmltag) {
65             $tagpair = "~<\s*/?\s*$htmltag\b\s*[^>]*>~";
66             $strippedtext = preg_replace($tagpair, '', $strippedtext);
67         }
69         $textarray = array();
70         preg_match_all($this->htmltstarttagsandattributes, $strippedtext, $textarray);
71         if ($textarray[0]) {
72             return $this->allowed_tags_message($textarray[0][0]);
73         }
75         preg_match_all($this->htmltclosetags, $strippedtext, $textarray);
76         if ($textarray[0]) {
77             return $this->allowed_tags_message($textarray[0][0]);
78         }
80         return '';
81     }
83     /**
84      * Returns a message indicating what tags are allowed.
85      *
86      * @param string $badtag The disallowed tag that was supplied
87      * @return string Message indicating what tags are allowed
88      */
89     private function allowed_tags_message($badtag) {
90         $a = new stdClass();
91         $a->tag = htmlspecialchars($badtag);
92         $a->allowed = $this->get_list_of_printable_allowed_tags($this->allowedhtmltags);
93         if ($a->allowed) {
94             return get_string('tagsnotallowed', 'qtype_gapselect', $a);
95         } else {
96             return get_string('tagsnotallowedatall', 'qtype_gapselect', $a);
97         }
98     }
100     /**
101      * Returns a prinatble list of allowed HTML tags.
102      *
103      * @param array $allowedhtmltags An array for tag strings that are allowed
104      * @return string A printable list of tags
105      */
106     private function get_list_of_printable_allowed_tags($allowedhtmltags) {
107         $allowedtaglist = array();
108         foreach ($allowedhtmltags as $htmltag) {
109             $allowedtaglist[] = htmlspecialchars('<' . $htmltag . '>');
110         }
111         return implode(', ', $allowedtaglist);
112     }
114     /**
115      * definition_inner adds all specific fields to the form.
116      * @param object $mform (the form being built).
117      */
118     protected function definition_inner($mform) {
119         global $CFG;
121         // Add the answer (choice) fields to the form.
122         $this->definition_answer_choice($mform);
124         $this->add_combined_feedback_fields(true);
125         $this->add_interactive_settings(true, true);
126     }
128     /**
129      * Defines form elements for answer choices.
130      *
131      * @param object $mform The Moodle form object being built
132      */
133     protected function definition_answer_choice(&$mform) {
134         $mform->addElement('header', 'choicehdr', get_string('choices', 'qtype_gapselect'));
135         $mform->setExpanded('choicehdr', 1);
137         $mform->addElement('checkbox', 'shuffleanswers', get_string('shuffle', 'qtype_gapselect'));
138         $mform->setDefault('shuffleanswers', 0);
140         $textboxgroup = array();
141         $textboxgroup[] = $mform->createElement('group', 'choices',
142                 get_string('choicex', 'qtype_gapselect'), $this->choice_group($mform));
144         if (isset($this->question->options)) {
145             $countanswers = count($this->question->options->answers);
146         } else {
147             $countanswers = 0;
148         }
150         if ($this->question->formoptions->repeatelements) {
151             $defaultstartnumbers = QUESTION_NUMANS_START * 2;
152             $repeatsatstart = max($defaultstartnumbers, QUESTION_NUMANS_START,
153                     $countanswers + QUESTION_NUMANS_ADD);
154         } else {
155             $repeatsatstart = $countanswers;
156         }
158         $repeatedoptions = $this->repeated_options();
159         $mform->setType('answer', PARAM_RAW);
160         $this->repeat_elements($textboxgroup, $repeatsatstart, $repeatedoptions,
161                 'noanswers', 'addanswers', QUESTION_NUMANS_ADD,
162                 get_string('addmorechoiceblanks', 'qtype_gapselect'), true);
163     }
165     /**
166      * Return how many different groups of choices there should be.
167      *
168      * @return int the maximum group number.
169      */
170     function get_maximum_choice_group_number() {
171         return 8;
172     }
174     /**
175      * Creates an array with elements for a choice group.
176      *
177      * @param object $mform The Moodle form we are working with
178      * @param int $maxgroup The number of max group generate element select.
179      * @return array Array for form elements
180      */
181     protected function choice_group($mform) {
182         $options = array();
183         for ($i = 1; $i <= $this->get_maximum_choice_group_number(); $i += 1) {
184             $options[$i] = $i;
185         }
186         $grouparray = array();
187         $grouparray[] = $mform->createElement('text', 'answer',
188                 get_string('answer', 'qtype_gapselect'), array('size' => 30, 'class' => 'tweakcss'));
189         $grouparray[] = $mform->createElement('select', 'choicegroup',
190                 get_string('group', 'qtype_gapselect'), $options);
191         return $grouparray;
192     }
194     /**
195      * Returns an array for form repeat options.
196      *
197      * @return array Array of repeate options
198      */
199     protected function repeated_options() {
200         $repeatedoptions = array();
201         $repeatedoptions['choicegroup']['default'] = '1';
202         $repeatedoptions['choices[answer]']['type'] = PARAM_RAW;
203         return $repeatedoptions;
204     }
206     public function data_preprocessing($question) {
207         $question = parent::data_preprocessing($question);
208         $question = $this->data_preprocessing_combined_feedback($question, true);
209         $question = $this->data_preprocessing_hints($question, true, true);
211         $question = $this->data_preprocessing_answers($question, true);
212         if (!empty($question->options->answers)) {
213             $key = 0;
214             foreach ($question->options->answers as $answer) {
215                 $question = $this->data_preprocessing_choice($question, $answer, $key);
216                 $key++;
217             }
218         }
220         if (!empty($question->options)) {
221             $question->shuffleanswers = $question->options->shuffleanswers;
222         }
224         return $question;
225     }
227     protected function data_preprocessing_choice($question, $answer, $key) {
228         // See comment in data_preprocessing_answers.
229         unset($this->_form->_defaultValues['choices[$key][choicegroup]']);
230         $question->choices[$key]['answer'] = $answer->answer;
231         $question->choices[$key]['choicegroup'] = $answer->feedback;
232         return $question;
233     }
235     public function validation($data, $files) {
236         $errors = parent::validation($data, $files);
237         $questiontext = $data['questiontext'];
238         $choices = $data['choices'];
240         // Check the whether the slots are valid.
241         $errorsinquestiontext = $this->validate_slots($questiontext['text'], $choices);
242         if ($errorsinquestiontext) {
243             $errors['questiontext'] = $errorsinquestiontext;
244         }
245         foreach ($choices as $key => $choice) {
246             $answer = $choice['answer'];
248             // Check whether the HTML tags are allowed tags.
249             $tagerror = $this->get_illegal_tag_error($answer);
250             if ($tagerror) {
251                 $errors['choices['.$key.']'] = $tagerror;
252             }
253         }
254         return $errors;
255     }
257     /**
258      * Finds errors in question slots.
259      *
260      * @param string $questiontext The question text
261      * @param array $choices Question choices
262      * @return string|bool Error message or false if no errors
263      */
264     private function validate_slots($questiontext, $choices) {
265         $error = 'Please check the Question text: ';
266         if (!$questiontext) {
267             return get_string('errorquestiontextblank', 'qtype_gapselect');
268         }
270         $matches = array();
271         preg_match_all($this->squarebracketsregex, $questiontext, $matches);
272         $slots = $matches[0];
274         if (!$slots) {
275             return get_string('errornoslots', 'qtype_gapselect');
276         }
278         $cleanedslots = array();
279         foreach ($slots as $slot) {
280             // The 2 is for'[[' and 4 is for '[[]]'.
281             $cleanedslots[] = substr($slot, 2, (strlen($slot) - 4));
282         }
283         $slots = $cleanedslots;
285         $found = false;
286         foreach ($slots as $slot) {
287             $found = false;
288             foreach ($choices as $key => $choice) {
289                 if ($slot == $key + 1) {
290                     if ($choice['answer'] === '') {
291                         return get_string('errorblankchoice', 'qtype_gapselect',
292                                 html_writer::tag('b', $slot));
293                     }
294                     $found = true;
295                     break;
296                 }
297             }
298             if (!$found) {
299                 return get_string('errormissingchoice', 'qtype_gapselect',
300                         html_writer::tag('b', $slot));
301             }
302         }
303         return false;
304     }
306     public function qtype() {
307         return '';
308     }