Merge branch 'MDL-64286' of https://github.com/timhunt/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         $question->choices[$key]['answer'] = $answer->answer;
229         $question->choices[$key]['choicegroup'] = $answer->feedback;
230         return $question;
231     }
233     public function validation($data, $files) {
234         $errors = parent::validation($data, $files);
235         $questiontext = $data['questiontext'];
236         $choices = $data['choices'];
238         // Check the whether the slots are valid.
239         $errorsinquestiontext = $this->validate_slots($questiontext['text'], $choices);
240         if ($errorsinquestiontext) {
241             $errors['questiontext'] = $errorsinquestiontext;
242         }
243         foreach ($choices as $key => $choice) {
244             $answer = $choice['answer'];
246             // Check whether the HTML tags are allowed tags.
247             $tagerror = $this->get_illegal_tag_error($answer);
248             if ($tagerror) {
249                 $errors['choices['.$key.']'] = $tagerror;
250             }
251         }
252         return $errors;
253     }
255     /**
256      * Finds errors in question slots.
257      *
258      * @param string $questiontext The question text
259      * @param array $choices Question choices
260      * @return string|bool Error message or false if no errors
261      */
262     private function validate_slots($questiontext, $choices) {
263         $error = 'Please check the Question text: ';
264         if (!$questiontext) {
265             return get_string('errorquestiontextblank', 'qtype_gapselect');
266         }
268         $matches = array();
269         preg_match_all($this->squarebracketsregex, $questiontext, $matches);
270         $slots = $matches[0];
272         if (!$slots) {
273             return get_string('errornoslots', 'qtype_gapselect');
274         }
276         $cleanedslots = array();
277         foreach ($slots as $slot) {
278             // The 2 is for'[[' and 4 is for '[[]]'.
279             $cleanedslots[] = substr($slot, 2, (strlen($slot) - 4));
280         }
281         $slots = $cleanedslots;
283         $found = false;
284         foreach ($slots as $slot) {
285             $found = false;
286             foreach ($choices as $key => $choice) {
287                 if ($slot == $key + 1) {
288                     if ($choice['answer'] === '') {
289                         return get_string('errorblankchoice', 'qtype_gapselect',
290                                 html_writer::tag('b', $slot));
291                     }
292                     $found = true;
293                     break;
294                 }
295             }
296             if (!$found) {
297                 return get_string('errormissingchoice', 'qtype_gapselect',
298                         html_writer::tag('b', $slot));
299             }
300         }
301         return false;
302     }
304     public function qtype() {
305         return '';
306     }