MDL-47494 ddimageortext: NOBUG more refactoring
[moodle.git] / question / type / ddimageortext / questiontypebase.php
CommitLineData
d2c112fd
JP
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 * Question type class for the drag-and-drop images onto images question type.
19 *
20 * @package qtype
21 * @subpackage ddimageortext
22 * @copyright 2009 The Open University
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24 */
25
26
27defined('MOODLE_INTERNAL') || die();
28
29require_once($CFG->libdir . '/questionlib.php');
30require_once($CFG->dirroot . '/question/engine/lib.php');
31require_once($CFG->dirroot . '/question/format/xml/format.php');
32require_once($CFG->dirroot . '/question/type/gapselect/questiontypebase.php');
33
34/**
35 * The drag-and-drop words into sentences question type class.
36 *
37 * @copyright 2009 The Open University
38 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
39 */
40class qtype_ddtoimage_base extends question_type {
41 protected function choice_group_key() {
42 return 'draggroup';
43 }
44
45 public function requires_qtypes() {
46 return array_merge(parent::requires_qtypes(), array('ddimageortext'));
47 }
48
49 public function get_question_options($question) {
50 global $DB;
51 $question->options = $DB->get_record('qtype_ddimageortext',
52 array('questionid' => $question->id), '*', MUST_EXIST);
53 $question->options->drags = $DB->get_records('qtype_ddimageortext_drags',
54 array('questionid' => $question->id), 'no ASC', '*');
55 $question->options->drops = $DB->get_records('qtype_ddimageortext_drops',
56 array('questionid' => $question->id), 'no ASC', '*');
57 parent::get_question_options($question);
58 }
59
60 protected function make_choice($dragdata) {
61 return new qtype_ddimageortext_drag_item($dragdata->label, $dragdata->no,
62 $dragdata->draggroup, $dragdata->infinite, $dragdata->id);
63 }
64
65 protected function make_place($dropzonedata) {
66 return new qtype_ddimageortext_drop_zone($dropzonedata->label, $dropzonedata->no,
67 $dropzonedata->group,
68 $dropzonedata->xleft, $dropzonedata->ytop);
69 }
70
71 protected function make_hint($hint) {
72 return question_hint_with_parts::load_from_record($hint);
73 }
74
75 protected function initialise_question_instance(question_definition $question, $questiondata) {
76 parent::initialise_question_instance($question, $questiondata);
77 $question->shufflechoices = $questiondata->options->shuffleanswers;
78
79 $this->initialise_combined_feedback($question, $questiondata, true);
80
81 $question->choices = array();
82 $choiceindexmap= array();
83
84 // Store the choices in arrays by group.
85 foreach ($questiondata->options->drags as $dragdata) {
86
87 $choice = $this->make_choice($dragdata);
88
89 if (array_key_exists($choice->choice_group(), $question->choices)) {
90 $question->choices[$choice->choice_group()][$dragdata->no] = $choice;
91 } else {
92 $question->choices[$choice->choice_group()][1] = $choice;
93 }
94
95 end($question->choices[$choice->choice_group()]);
96 $choiceindexmap[$dragdata->no] = array($choice->choice_group(),
97 key($question->choices[$choice->choice_group()]));
98 }
99
100 $question->places = array();
101 $question->rightchoices = array();
102
103 $i = 1;
104
105 foreach ($questiondata->options->drops as $dropdata) {
106 list($group, $choiceindex) = $choiceindexmap[$dropdata->choice];
107 $dropdata->group = $group;
108 $question->places[$dropdata->no] = $this->make_place($dropdata);
109 $question->rightchoices[$dropdata->no] = $choiceindex;
110 }
111 }
112
113 public function save_question_options($formdata) {
114 global $DB, $USER;
115 $context = $formdata->context;
116
117 $options = $DB->get_record('qtype_ddimageortext', array('questionid' => $formdata->id));
118 if (!$options) {
119 $options = new stdClass();
120 $options->questionid = $formdata->id;
121 $options->correctfeedback = '';
122 $options->partiallycorrectfeedback = '';
123 $options->incorrectfeedback = '';
124 $options->id = $DB->insert_record('qtype_ddimageortext', $options);
125 }
126
127 $options->shuffleanswers = !empty($formdata->shuffleanswers);
128 $options = $this->save_combined_feedback_helper($options, $formdata, $context, true);
129 $this->save_hints($formdata, true);
130 $DB->update_record('qtype_ddimageortext', $options);
131 $DB->delete_records('qtype_ddimageortext_drops', array('questionid' => $formdata->id));
132 foreach (array_keys($formdata->drops) as $dropno) {
133 if ($formdata->drops[$dropno]['choice'] == 0) {
134 continue;
135 }
136 $drop = new stdClass();
137 $drop->questionid = $formdata->id;
138 $drop->no = $dropno + 1;
139 $drop->xleft = $formdata->drops[$dropno]['xleft'];
140 $drop->ytop = $formdata->drops[$dropno]['ytop'];
141 $drop->choice = $formdata->drops[$dropno]['choice'];
142 $drop->label = $formdata->drops[$dropno]['droplabel'];
143
144 $DB->insert_record('qtype_ddimageortext_drops', $drop);
145 }
146
147 //an array of drag no -> drag id
148 $olddragids = $DB->get_records_menu('qtype_ddimageortext_drags',
149 array('questionid' => $formdata->id),
150 '', 'no, id');
151 foreach (array_keys($formdata->drags) as $dragno) {
152 $info = file_get_draft_area_info($formdata->dragitem[$dragno]);
153 if ($info['filecount'] > 0 || !empty($formdata->drags[$dragno]['draglabel'])) {
154 $draftitemid = $formdata->dragitem[$dragno];
155
156 $drag = new stdClass();
157 $drag->questionid = $formdata->id;
158 $drag->no = $dragno + 1;
159 $drag->draggroup = $formdata->drags[$dragno]['draggroup'];
160 $drag->infinite = empty($formdata->drags[$dragno]['infinite'])? 0 : 1;
161 $drag->label = $formdata->drags[$dragno]['draglabel'];
162
163 if (isset($olddragids[$dragno +1])) {
164 $drag->id = $olddragids[$dragno +1];
165 unset($olddragids[$dragno +1]);
166 $DB->update_record('qtype_ddimageortext_drags', $drag);
167 } else {
168 $drag->id = $DB->insert_record('qtype_ddimageortext_drags', $drag);
169 }
170
171 if ($formdata->dragitemtype[$dragno] == 'image') {
172 self::constrain_image_size_in_draft_area($draftitemid,
173 QTYPE_DDIMAGEORTEXT_DRAGIMAGE_MAXWIDTH,
174 QTYPE_DDIMAGEORTEXT_DRAGIMAGE_MAXHEIGHT);
175 file_save_draft_area_files($draftitemid, $formdata->context->id,
176 'qtype_ddimageortext', 'dragimage', $drag->id,
177 array('subdirs' => 0, 'maxbytes' => 0, 'maxfiles' => 1));
178 } else {
179 //delete any existing files for draggable text item type
180 $fs = get_file_storage();
181 $fs->delete_area_files($formdata->context->id, 'qtype_ddimageortext',
182 'dragimage', $drag->id);
183 }
184
185 }
186
187 }
188 if (!empty($olddragids)) {
189 list($sql, $params) = $DB->get_in_or_equal(array_values($olddragids));
190 $DB->delete_records_select('qtype_ddimageortext_drags', "id $sql", $params);
191 }
192
193 self::constrain_image_size_in_draft_area($formdata->bgimage,
194 QTYPE_DDIMAGEORTEXT_BGIMAGE_MAXWIDTH,
195 QTYPE_DDIMAGEORTEXT_BGIMAGE_MAXHEIGHT);
196 file_save_draft_area_files($formdata->bgimage, $formdata->context->id,
197 'qtype_ddimageortext', 'bgimage', $formdata->id,
198 array('subdirs' => 0, 'maxbytes' => 0, 'maxfiles' => 1));
199 }
200
201
202 public static function constrain_image_size_in_draft_area($draftitemid, $maxwidth, $maxheight) {
203 global $USER;
204 $usercontext = get_context_instance(CONTEXT_USER, $USER->id);
205 $fs = get_file_storage();
206 $draftfiles = $fs->get_area_files($usercontext->id, 'user', 'draft', $draftitemid, 'id');
207 if ($draftfiles) {
208 foreach ($draftfiles as $file) {
209 if ($file->is_directory()) {
210 continue;
211 }
212 $imageinfo = $file->get_imageinfo();
213 $width = $imageinfo['width'];
214 $height = $imageinfo['height'];
215 $mimetype = $imageinfo['mimetype'];
216 switch ($mimetype) {
217 case 'image/jpeg' :
218 $quality = 80;
219 break;
220 case 'image/png' :
221 $quality = 8;
222 break;
223 default :
224 $quality = null;
225 }
226 $newwidth = min($maxwidth, $width);
227 $newheight = min($maxheight, $height);
228 if ($newwidth != $width || $newheight != $height) {
229 $newimagefilename = $file->get_filename();
230 $newimagefilename =
231 preg_replace('!\.!', "_{$newwidth}x{$newheight}.", $newimagefilename, 1);
232 $newrecord = new stdClass();
233 $newrecord->contextid = $usercontext->id;
234 $newrecord->component = 'user';
235 $newrecord->filearea = 'draft';
236 $newrecord->itemid = $draftitemid;
237 $newrecord->filepath = '/';
238 $newrecord->filename = $newimagefilename;
239 $fs->convert_image($newrecord, $file, $newwidth, $newheight, true, $quality);
240 $file->delete();
241 }
242 }
243 }
244 }
245 public function move_files($questionid, $oldcontextid, $newcontextid) {
246 global $DB;
247 $fs = get_file_storage();
248
249 parent::move_files($questionid, $oldcontextid, $newcontextid);
250 $fs->move_area_files_to_new_context($oldcontextid,
251 $newcontextid, 'qtype_ddimageortext', 'bgimage', $questionid);
252 $dragids = $DB->get_records_menu('qtype_ddimageortext_drags',
253 array('questionid' => $questionid), 'id', 'id,1');
254 foreach ($dragids as $dragid => $notused) {
255 $fs->move_area_files_to_new_context($oldcontextid,
256 $newcontextid, 'qtype_ddimageortext', 'dragimage', $dragid);
257 }
258
259 $this->move_files_in_combined_feedback($questionid, $oldcontextid, $newcontextid);
260 }
261
262 /**
263 * Delete all the files belonging to this question.
264 * @param int $questionid the question being deleted.
265 * @param int $contextid the context the question is in.
266 */
267
268 protected function delete_files($questionid, $contextid) {
269 global $DB;
270 $fs = get_file_storage();
271
272 parent::delete_files($questionid, $contextid);
273
274 $dragids = $DB->get_records_menu('qtype_ddimageortext_drags',
275 array('questionid' => $questionid), 'id', 'id,1');
276 foreach ($dragids as $dragid => $notused) {
277 $fs->delete_area_files($contextid, 'qtype_ddimageortext', 'dragimage', $dragid);
278 }
279
280 $this->delete_files_in_combined_feedback($questionid, $contextid);
281 }
282
283 public function export_to_xml($question, $format, $extra = null) {
284 $fs = get_file_storage();
285 $contextid = $question->contextid;
286 $output = '';
287
288 if ($question->options->shuffleanswers) {
289 $output .= " <shuffleanswers/>\n";
290 }
291 $output .= $format->write_combined_feedback($question->options);
292 $output .= $format->write_hints($question);
293 $files = $fs->get_area_files($contextid, 'qtype_ddimageortext', 'bgimage', $question->id);
294 $output .= " ".$this->write_files($files, 2)."\n";;
295
296 foreach ($question->options->drags as $drag) {
297 $files =
298 $fs->get_area_files($contextid, 'qtype_ddimageortext', 'dragimage', $drag->id);
299 $output .= " <drag>\n";
300 $output .= " <no>{$drag->no}</no>\n";
301 $output .= $format->writetext($drag->label, 3)."\n";
302 $output .= " <draggroup>{$drag->draggroup}</draggroup>\n";
303 if ($drag->infinite) {
304 $output .= " <infinite/>\n";
305 }
306 $output .= $this->write_files($files, 3);
307 $output .= " </drag>\n";
308 }
309 foreach ($question->options->drops as $drop) {
310 $output .= " <drop>\n";
311 $output .= $format->writetext($drop->label, 3);
312 $output .= " <no>{$drop->no}</no>\n";
313 $output .= " <choice>{$drop->choice}</choice>\n";
314 $output .= " <xleft>{$drop->xleft}</xleft>\n";
315 $output .= " <ytop>{$drop->ytop}</ytop>\n";
316 $output .= " </drop>\n";
317 }
318
319 return $output;
320 }
321
322 public function import_from_xml($data, $question, $format, $extra=null) {
323 if (!isset($data['@']['type']) || $data['@']['type'] != 'ddimageortext') {
324 return false;
325 }
326
327 $question = $format->import_headers($data);
328 $question->qtype = 'ddimageortext';
329
330 $question->shuffleanswers = array_key_exists('shuffleanswers',
331 $format->getpath($data, array('#'), array()));
332
333 $filexml = $format->getpath($data, array('#', 'file'), array());
334 $question->bgimage = $this->import_files_to_draft_file_area($format, $filexml);
335 $drags = $data['#']['drag'];
336 $question->drags = array();
337
338 foreach ($drags as $dragxml) {
339 $dragno = $format->getpath($dragxml, array('#', 'no', 0, '#'), 0);
340 $dragindex = $dragno -1;
341 $question->drags[$dragindex] = array();
342 $question->drags[$dragindex]['draglabel'] =
343 $format->getpath($dragxml, array('#', 'text', 0, '#'), '', true);
344 $question->drags[$dragindex]['infinite'] = array_key_exists('infinite', $dragxml['#']);
345 $question->drags[$dragindex]['draggroup'] =
346 $format->getpath($dragxml, array('#', 'draggroup', 0, '#'), 1);
347 $filexml = $format->getpath($dragxml, array('#', 'file'), array());
348 $question->dragitem[$dragindex] =
349 $this->import_files_to_draft_file_area($format, $filexml);
350 if (count($filexml)) {
351 $question->dragitemtype[$dragindex] = 'image';
352 } else {
353 $question->dragitemtype[$dragindex] = 'word';
354 }
355 }
356
357 $drops = $data['#']['drop'];
358 $question->drops = array();
359 foreach ($drops as $dropxml) {
360 $dropno = $format->getpath($dropxml, array('#', 'no', 0, '#'), 0);
361 $dropindex = $dropno -1;
362 $question->drops[$dropindex] = array();
363 $question->drops[$dropindex]['choice'] =
364 $format->getpath($dropxml, array('#', 'choice', 0, '#'), 0);
365 $question->drops[$dropindex]['droplabel'] =
366 $format->getpath($dropxml, array('#', 'text', 0, '#'), '', true);
367 $question->drops[$dropindex]['xleft'] =
368 $format->getpath($dropxml, array('#', 'xleft', 0, '#'), '');
369 $question->drops[$dropindex]['ytop'] =
370 $format->getpath($dropxml, array('#', 'ytop', 0, '#'), '');
371 }
372
373 $format->import_combined_feedback($question, $data, true);
374 $format->import_hints($question, $data, true);
375
376 return $question;
377 }
378
379
380 /**
381 * Create a draft files area, import files into it and return the draft item id.
382 * @param qformat_xml $format
383 * @param array $xml an array of <file> nodes from the the parsed XML.
384 * @return integer draftitemid
385 */
386 public function import_files_to_draft_file_area($format, $xml) {
387 global $USER;
388 $fs = get_file_storage();
389 $files = $format->import_files($xml);
390 $usercontext = get_context_instance(CONTEXT_USER, $USER->id);
391 $draftitemid = file_get_unused_draft_itemid();
392 foreach ($files as $file) {
393 $record = new stdClass();
394 $record->contextid = $usercontext->id;
395 $record->component = 'user';
396 $record->filearea = 'draft';
397 $record->itemid = $draftitemid;
398 $record->filename = $file->name;
399 $record->filepath = '/';
400 $fs->create_file_from_string($record, $this->decode_file($file));
401 }
402 return $draftitemid;
403 }
404
405 /**
406 * Convert files into text output in the given format.
407 * This method is copied from qformat_default as a quick fix, as the method there is
408 * protected.
409 * @param array
410 * @param string encoding method
411 * @return string $string
412 */
413 public function write_files($files, $indent) {
414 if (empty($files)) {
415 return '';
416 }
417 $string = '';
418 foreach ($files as $file) {
419 if ($file->is_directory()) {
420 continue;
421 }
422 $string .= str_repeat(' ', $indent);
423 $string .= '<file name="' . $file->get_filename() . '" encoding="base64">';
424 $string .= base64_encode($file->get_content());
425 $string .= "</file>\n";
426 }
427 return $string;
428 }
429
430 public function get_possible_responses($questiondata) {
431 $question = $this->make_question($questiondata);
432
433 $parts = array();
434 foreach ($question->places as $placeno => $place) {
435 $group = $place->group;
436 $choices = array();
437
438 foreach ($question->choices[$group] as $i => $choice) {
439 $summarisechoice = $choice->summarise();
440
441 $correct = $question->rightchoices[$placeno] == $i;
442 $choices[$choice->no] = new question_possible_response(
443 $summarisechoice,
444 $correct?1:0);
445 }
446 $choices[null] = question_possible_response::no_response();
447
448 $parts[$placeno] = $choices;
449 }
450
451 return $parts;
452 }
453
454 public function get_random_guess_score($questiondata) {
455 $question = $this->make_question($questiondata);
456 return $question->get_random_guess_score();
457 }
458
459}