Merge branch 'wip-MDL-32108-master' of git://github.com/abgreeve/moodle
[moodle.git] / mod / lesson / format.php
CommitLineData
472e5662 1<?php
0a4abb73
SH
2
3// This file is part of Moodle - http://moodle.org/
4//
5// Moodle is free software: you can redistribute it and/or modify
6// it under the terms of the GNU General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9//
10// Moodle is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13// GNU General Public License for more details.
14//
15// You should have received a copy of the GNU General Public License
16// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17
5491947a 18/**
86342d63 19 * format.php - Default format class for file imports/exports. Doesn't do
5491947a 20 * everything on it's own -- it needs to be extended.
21 *
0a4abb73
SH
22 * Included by import.ph
23 *
cc3dbaaa
PS
24 * @package mod
25 * @subpackage lesson
26 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
27 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
0a4abb73
SH
28 **/
29
1e7f8ea2
PS
30defined('MOODLE_INTERNAL') || die();
31
55190d7e
TH
32/**#@+
33 * The core question types.
34 *
35 * These used to be in lib/questionlib.php, but are being deprecated. Copying them
36 * here to keep this code working for now.
37 */
38if (!defined('SHORTANSWER')) {
39 define("SHORTANSWER", "shortanswer");
40 define("TRUEFALSE", "truefalse");
41 define("MULTICHOICE", "multichoice");
42 define("RANDOM", "random");
43 define("MATCH", "match");
44 define("RANDOMSAMATCH", "randomsamatch");
45 define("DESCRIPTION", "description");
46 define("NUMERICAL", "numerical");
47 define("MULTIANSWER", "multianswer");
48 define("CALCULATED", "calculated");
49 define("ESSAY", "essay");
50}
51/**#@-*/
52
0a4abb73
SH
53/**
54 * Given some question info and some data about the the answers
55 * this function parses, organises and saves the question
56 *
57 * This is only used when IMPORTING questions and is only called
58 * from format.php
59 * Lifted from mod/quiz/lib.php -
60 * 1. all reference to oldanswers removed
61 * 2. all reference to quiz_multichoice table removed
62 * 3. In SHORTANSWER questions usecase is store in the qoption field
63 * 4. In NUMERIC questions store the range as two answers
64 * 5. TRUEFALSE options are ignored
65 * 6. For MULTICHOICE questions with more than one answer the qoption field is true
66 *
67 * @param opject $question Contains question data like question, type and answers.
68 * @return object Returns $result->error or $result->notice.
5491947a 69 **/
0a4abb73
SH
70function lesson_save_question_options($question, $lesson) {
71 global $DB;
72
73 // These lines are required to ensure that all page types have
74 // been loaded for the following switch
75 if (!($lesson instanceof lesson)) {
76 $lesson = new lesson($lesson);
77 }
78 $manager = lesson_page_type_manager::get($lesson);
2f67a9b3 79
0a4abb73 80 $timenow = time();
39790bd8 81 $result = new stdClass();
0a4abb73
SH
82 switch ($question->qtype) {
83 case LESSON_PAGE_SHORTANSWER:
84
85 $answers = array();
86 $maxfraction = -1;
87
88 // Insert all the new answers
89 foreach ($question->answer as $key => $dataanswer) {
90 if ($dataanswer != "") {
91 $answer = new stdClass;
92 $answer->lessonid = $question->lessonid;
93 $answer->pageid = $question->id;
94 if ($question->fraction[$key] >=0.5) {
95 $answer->jumpto = LESSON_NEXTPAGE;
96 }
97 $answer->timecreated = $timenow;
98 $answer->grade = $question->fraction[$key] * 100;
99 $answer->answer = $dataanswer;
912ea4bc
RT
100 $answer->response = $question->feedback[$key]['text'];
101 $answer->responseformat = $question->feedback[$key]['format'];
0a4abb73
SH
102 $answer->id = $DB->insert_record("lesson_answers", $answer);
103 $answers[] = $answer->id;
104 if ($question->fraction[$key] > $maxfraction) {
105 $maxfraction = $question->fraction[$key];
106 }
107 }
108 }
109
110
111 /// Perform sanity checks on fractional grades
112 if ($maxfraction != 1) {
113 $maxfraction = $maxfraction * 100;
114 $result->notice = get_string("fractionsnomax", "quiz", $maxfraction);
115 return $result;
116 }
117 break;
118
119 case LESSON_PAGE_NUMERICAL: // Note similarities to SHORTANSWER
120
121 $answers = array();
122 $maxfraction = -1;
123
124
125 // for each answer store the pair of min and max values even if they are the same
126 foreach ($question->answer as $key => $dataanswer) {
127 if ($dataanswer != "") {
128 $answer = new stdClass;
129 $answer->lessonid = $question->lessonid;
130 $answer->pageid = $question->id;
131 $answer->jumpto = LESSON_NEXTPAGE;
132 $answer->timecreated = $timenow;
133 $answer->grade = $question->fraction[$key] * 100;
134 $min = $question->answer[$key] - $question->tolerance[$key];
135 $max = $question->answer[$key] + $question->tolerance[$key];
136 $answer->answer = $min.":".$max;
137 // $answer->answer = $question->min[$key].":".$question->max[$key]; original line for min/max
912ea4bc
RT
138 $answer->response = $question->feedback[$key]['text'];
139 $answer->responseformat = $question->feedback[$key]['format'];
0a4abb73
SH
140 $answer->id = $DB->insert_record("lesson_answers", $answer);
141
142 $answers[] = $answer->id;
143 if ($question->fraction[$key] > $maxfraction) {
144 $maxfraction = $question->fraction[$key];
145 }
146 }
147 }
148
149 /// Perform sanity checks on fractional grades
150 if ($maxfraction != 1) {
151 $maxfraction = $maxfraction * 100;
152 $result->notice = get_string("fractionsnomax", "quiz", $maxfraction);
153 return $result;
154 }
155 break;
156
157
158 case LESSON_PAGE_TRUEFALSE:
159
160 // the truth
92701024 161 $answer = new stdClass();
0a4abb73
SH
162 $answer->lessonid = $question->lessonid;
163 $answer->pageid = $question->id;
164 $answer->timecreated = $timenow;
165 $answer->answer = get_string("true", "quiz");
912ea4bc 166 $answer->grade = $question->correctanswer * 100;
0a4abb73
SH
167 if ($answer->grade > 50 ) {
168 $answer->jumpto = LESSON_NEXTPAGE;
169 }
170 if (isset($question->feedbacktrue)) {
912ea4bc
RT
171 $answer->response = $question->feedbacktrue['text'];
172 $answer->responseformat = $question->feedbacktrue['format'];
0a4abb73 173 }
64a3ce8c 174 $DB->insert_record("lesson_answers", $answer);
0a4abb73
SH
175
176 // the lie
177 $answer = new stdClass;
178 $answer->lessonid = $question->lessonid;
179 $answer->pageid = $question->id;
180 $answer->timecreated = $timenow;
181 $answer->answer = get_string("false", "quiz");
912ea4bc 182 $answer->grade = (1 - (int)$question->correctanswer) * 100;
0a4abb73
SH
183 if ($answer->grade > 50 ) {
184 $answer->jumpto = LESSON_NEXTPAGE;
185 }
186 if (isset($question->feedbackfalse)) {
912ea4bc
RT
187 $answer->response = $question->feedbackfalse['text'];
188 $answer->responseformat = $question->feedbackfalse['format'];
0a4abb73 189 }
64a3ce8c 190 $DB->insert_record("lesson_answers", $answer);
0a4abb73
SH
191
192 break;
193
194 case LESSON_PAGE_MULTICHOICE:
195
196 $totalfraction = 0;
197 $maxfraction = -1;
198
199 $answers = array();
200
201 // Insert all the new answers
202 foreach ($question->answer as $key => $dataanswer) {
203 if ($dataanswer != "") {
204 $answer = new stdClass;
205 $answer->lessonid = $question->lessonid;
206 $answer->pageid = $question->id;
207 $answer->timecreated = $timenow;
208 $answer->grade = $question->fraction[$key] * 100;
209 // changed some defaults
210 /* Original Code
211 if ($answer->grade > 50 ) {
212 $answer->jumpto = LESSON_NEXTPAGE;
213 }
214 Replaced with: */
215 if ($answer->grade > 50 ) {
216 $answer->jumpto = LESSON_NEXTPAGE;
217 $answer->score = 1;
218 }
219 // end Replace
912ea4bc
RT
220 $answer->answer = $dataanswer['text'];
221 $answer->answerformat = $dataanswer['format'];
222 $answer->response = $question->feedback[$key]['text'];
223 $answer->responseformat = $question->feedback[$key]['format'];
0a4abb73
SH
224 $answer->id = $DB->insert_record("lesson_answers", $answer);
225 // for Sanity checks
226 if ($question->fraction[$key] > 0) {
227 $totalfraction += $question->fraction[$key];
228 }
229 if ($question->fraction[$key] > $maxfraction) {
230 $maxfraction = $question->fraction[$key];
231 }
232 }
233 }
234
235 /// Perform sanity checks on fractional grades
236 if ($question->single) {
237 if ($maxfraction != 1) {
238 $maxfraction = $maxfraction * 100;
239 $result->notice = get_string("fractionsnomax", "quiz", $maxfraction);
240 return $result;
241 }
242 } else {
243 $totalfraction = round($totalfraction,2);
244 if ($totalfraction != 1) {
245 $totalfraction = $totalfraction * 100;
246 $result->notice = get_string("fractionsaddwrong", "quiz", $totalfraction);
247 return $result;
248 }
249 }
250 break;
251
252 case LESSON_PAGE_MATCHING:
253
254 $subquestions = array();
255
256 $defaultanswer = new stdClass;
257 $defaultanswer->lessonid = $question->lessonid;
258 $defaultanswer->pageid = $question->id;
259 $defaultanswer->timecreated = $timenow;
260 $defaultanswer->grade = 0;
261
262 // The first answer should always be the correct answer
263 $correctanswer = clone($defaultanswer);
264 $correctanswer->answer = get_string('thatsthecorrectanswer', 'lesson');
265 $DB->insert_record("lesson_answers", $correctanswer);
266
267 // The second answer should always be the wrong answer
268 $wronganswer = clone($defaultanswer);
269 $wronganswer->answer = get_string('thatsthewronganswer', 'lesson');
270 $DB->insert_record("lesson_answers", $wronganswer);
271
272 $i = 0;
273 // Insert all the new question+answer pairs
274 foreach ($question->subquestions as $key => $questiontext) {
275 $answertext = $question->subanswers[$key];
276 if (!empty($questiontext) and !empty($answertext)) {
277 $answer = clone($defaultanswer);
912ea4bc
RT
278 $answer->answer = $questiontext['text'];
279 $answer->answerformat = $questiontext['format'];
0a4abb73
SH
280 $answer->response = $answertext;
281 if ($i == 0) {
282 // first answer contains the correct answer jump
283 $answer->jumpto = LESSON_NEXTPAGE;
284 }
64a3ce8c 285 $subquestions[] = $DB->insert_record("lesson_answers", $answer);
0a4abb73
SH
286 $i++;
287 }
288 }
289
290 if (count($subquestions) < 3) {
291 $result->notice = get_string("notenoughsubquestions", "quiz");
292 return $result;
293 }
294 break;
295 default:
296 $result->error = "Unsupported question type ($question->qtype)!";
297 return $result;
298 }
299 return true;
300}
394c97c8 301
394c97c8 302
90455bb3 303class qformat_default {
394c97c8 304
305 var $displayerrors = true;
306 var $category = NULL;
307 var $questionids = array();
0a4abb73
SH
308 var $qtypeconvert = array(NUMERICAL => LESSON_PAGE_NUMERICAL,
309 MULTICHOICE => LESSON_PAGE_MULTICHOICE,
310 TRUEFALSE => LESSON_PAGE_TRUEFALSE,
311 SHORTANSWER => LESSON_PAGE_SHORTANSWER,
312 MATCH => LESSON_PAGE_MATCHING
90455bb3 313 );
394c97c8 314
0a4abb73
SH
315 // Importing functions
316 function provide_import() {
317 return false;
318 }
394c97c8 319
320 function importpreprocess() {
0a4abb73 321 // Does any pre-processing that may be desired
394c97c8 322 return true;
323 }
324
325 function importprocess($filename, $lesson, $pageid) {
d68ccdba 326 global $DB, $OUTPUT;
86342d63 327
394c97c8 328 /// Processes a given file. There's probably little need to change this
329 $timenow = time();
330
331 if (! $lines = $this->readdata($filename)) {
d68ccdba 332 echo $OUTPUT->notification("File could not be read, or was empty");
394c97c8 333 return false;
334 }
335
336 if (! $questions = $this->readquestions($lines)) { // Extract all the questions
d68ccdba 337 echo $OUTPUT->notification("There are no questions in this file!");
394c97c8 338 return false;
339 }
86342d63 340
912ea4bc
RT
341 //Avoid category as question type
342 echo $OUTPUT->notification(get_string('importcount', 'lesson',
343 $this->count_questions($questions)), 'notifysuccess');
394c97c8 344
345 $count = 0;
346
0a4abb73
SH
347 $unsupportedquestions = 0;
348
394c97c8 349 foreach ($questions as $question) { // Process and store each question
350 switch ($question->qtype) {
912ea4bc
RT
351 //TODO: Bad way to bypass category in data... Quickfix for MDL-27964
352 case 'category':
353 break;
394c97c8 354 // the good ones
355 case SHORTANSWER :
356 case NUMERICAL :
357 case TRUEFALSE :
358 case MULTICHOICE :
359 case MATCH :
360 $count++;
361
912ea4bc
RT
362 //Show nice formated question in one line.
363 echo "<hr><p><b>$count</b>. ".$this->format_question_text($question)."</p>";
364
f7ffb898 365 $newpage = new stdClass;
394c97c8 366 $newpage->lessonid = $lesson->id;
90455bb3 367 $newpage->qtype = $this->qtypeconvert[$question->qtype];
394c97c8 368 switch ($question->qtype) {
369 case SHORTANSWER :
90455bb3 370 if (isset($question->usecase)) {
371 $newpage->qoption = $question->usecase;
372 }
394c97c8 373 break;
374 case MULTICHOICE :
375 if (isset($question->single)) {
376 $newpage->qoption = !$question->single;
377 }
378 break;
379 }
380 $newpage->timecreated = $timenow;
381 if ($question->name != $question->questiontext) {
382 $newpage->title = $question->name;
383 } else {
384 $newpage->title = "Page $count";
385 }
386 $newpage->contents = $question->questiontext;
387
388 // set up page links
389 if ($pageid) {
390 // the new page follows on from this page
646fc290 391 if (!$page = $DB->get_record("lesson_pages", array("id" => $pageid))) {
771dc7b2 392 print_error('invalidpageid', 'lesson');
394c97c8 393 }
394 $newpage->prevpageid = $pageid;
395 $newpage->nextpageid = $page->nextpageid;
396 // insert the page and reset $pageid
6d40f12e 397 $newpageid = $DB->insert_record("lesson_pages", $newpage);
394c97c8 398 // update the linked list
6d40f12e 399 $DB->set_field("lesson_pages", "nextpageid", $newpageid, array("id" => $pageid));
394c97c8 400
401 } else {
402 // new page is the first page
403 // get the existing (first) page (if any)
646fc290 404 $params = array ("lessonid" => $lesson->id, "prevpageid" => 0);
405 if (!$page = $DB->get_record_select("lesson_pages", "lessonid = :lessonid AND prevpageid = :prevpageid", $params)) {
394c97c8 406 // there are no existing pages
407 $newpage->prevpageid = 0; // this is a first page
408 $newpage->nextpageid = 0; // this is the only page
646fc290 409 $newpageid = $DB->insert_record("lesson_pages", $newpage);
394c97c8 410 } else {
411 // there are existing pages put this at the start
412 $newpage->prevpageid = 0; // this is a first page
413 $newpage->nextpageid = $page->id;
646fc290 414 $newpageid = $DB->insert_record("lesson_pages", $newpage);
394c97c8 415 // update the linked list
6d40f12e 416 $DB->set_field("lesson_pages", "prevpageid", $newpageid, array("id" => $page->id));
394c97c8 417 }
418 }
419 // reset $pageid and put the page ID in $question, used in save_question_option()
420 $pageid = $newpageid;
421 $question->id = $newpageid;
86342d63 422
394c97c8 423 $this->questionids[] = $question->id;
424
425 // Now to save all the answers and type-specific options
426
427 $question->lessonid = $lesson->id; // needed for foreign key
90455bb3 428 $question->qtype = $this->qtypeconvert[$question->qtype];
0a4abb73 429 $result = lesson_save_question_options($question, $lesson);
394c97c8 430
431 if (!empty($result->error)) {
d68ccdba 432 echo $OUTPUT->notification($result->error);
394c97c8 433 return false;
434 }
435
436 if (!empty($result->notice)) {
d68ccdba 437 echo $OUTPUT->notification($result->notice);
394c97c8 438 return true;
439 }
440 break;
441 // the Bad ones
442 default :
0a4abb73
SH
443 $unsupportedquestions++;
444 break;
394c97c8 445 }
86342d63 446
394c97c8 447 }
0a4abb73
SH
448 if ($unsupportedquestions) {
449 echo $OUTPUT->notification(get_string('unknownqtypesnotimported', 'lesson', $unsupportedquestions));
450 }
394c97c8 451 return true;
452 }
453
912ea4bc
RT
454 /**
455 * Count all non-category questions in the questions array.
456 *
457 * @param array questions An array of question objects.
458 * @return int The count.
459 *
460 */
461 protected function count_questions($questions) {
462 $count = 0;
463 if (!is_array($questions)) {
464 return $count;
465 }
466 foreach ($questions as $question) {
467 if (!is_object($question) || !isset($question->qtype) ||
468 ($question->qtype == 'category')) {
469 continue;
470 }
471 $count++;
472 }
473 return $count;
474 }
394c97c8 475
476 function readdata($filename) {
477 /// Returns complete file with an array, one item per line
478
479 if (is_readable($filename)) {
480 $filearray = file($filename);
481
482 /// Check for Macintosh OS line returns (ie file on one line), and fix
6dbcacee 483 if (preg_match("/\r/", $filearray[0]) AND !preg_match("/\n/", $filearray[0])) {
394c97c8 484 return explode("\r", $filearray[0]);
485 } else {
486 return $filearray;
487 }
488 }
489 return false;
490 }
491
ba15c346 492 protected function readquestions($lines) {
86342d63
PS
493 /// Parses an array of lines into an array of questions,
494 /// where each item is a question object as defined by
495 /// readquestion(). Questions are defined as anything
394c97c8 496 /// between blank lines.
86342d63 497
394c97c8 498 $questions = array();
499 $currentquestion = array();
500
501 foreach ($lines as $line) {
502 $line = trim($line);
503 if (empty($line)) {
504 if (!empty($currentquestion)) {
505 if ($question = $this->readquestion($currentquestion)) {
506 $questions[] = $question;
507 }
508 $currentquestion = array();
509 }
510 } else {
511 $currentquestion[] = $line;
512 }
513 }
514
515 if (!empty($currentquestion)) { // There may be a final question
516 if ($question = $this->readquestion($currentquestion)) {
517 $questions[] = $question;
518 }
519 }
520
521 return $questions;
522 }
523
524
525 function readquestion($lines) {
86342d63
PS
526 /// Given an array of lines known to define a question in
527 /// this format, this function converts it into a question
394c97c8 528 /// object suitable for processing and insertion into Moodle.
529
530 echo "<p>This flash question format has not yet been completed!</p>";
531
532 return NULL;
533 }
534
90455bb3 535 function defaultquestion() {
536 // returns an "empty" question
537 // Somewhere to specify question parameters that are not handled
538 // by import but are required db fields.
86342d63 539 // This should not be overridden.
508fe4d8 540 global $CFG;
541
90455bb3 542 $question = new stdClass();
0a4abb73 543 $question->shuffleanswers = get_config('quiz', 'shuffleanswers');
912ea4bc 544 $question->defaultmark = 1;
508fe4d8 545 $question->image = "";
546 $question->usecase = 0;
547 $question->multiplier = array();
548 $question->generalfeedback = '';
549 $question->correctfeedback = '';
550 $question->partiallycorrectfeedback = '';
551 $question->incorrectfeedback = '';
552 $question->answernumbering = 'abc';
553 $question->penalty = 0.1;
554 $question->length = 1;
90455bb3 555 $question->qoption = 0;
556 $question->layout = 1;
86342d63 557
912ea4bc
RT
558 // this option in case the questiontypes class wants
559 // to know where the data came from
560 $question->export_process = true;
561 $question->import_process = true;
562
90455bb3 563 return $question;
564 }
394c97c8 565
566 function importpostprocess() {
0a4abb73
SH
567 /// Does any post-processing that may be desired
568 /// Argument is a simple array of question ids that
569 /// have just been added.
394c97c8 570 return true;
571 }
572
912ea4bc
RT
573 /**
574 * Convert the question text to plain text, so it can safely be displayed
575 * during import to let the user see roughly what is going on.
576 */
577 protected function format_question_text($question) {
578 $formatoptions = new stdClass();
579 $formatoptions->noclean = true;
580 return html_to_text(format_text($question->questiontext,
581 $question->questiontextformat, $formatoptions), 0, false);
582 }
394c97c8 583}
584
86342d63 585