format.php 35 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021
  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. * Defines the base class for question import and export formats.
  18. *
  19. * @package moodlecore
  20. * @subpackage questionbank
  21. * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
  22. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23. */
  24. defined('MOODLE_INTERNAL') || die();
  25. /**
  26. * Base class for question import and export formats.
  27. *
  28. * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
  29. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  30. */
  31. class qformat_default {
  32. public $displayerrors = true;
  33. public $category = null;
  34. public $questions = array();
  35. public $course = null;
  36. public $filename = '';
  37. public $realfilename = '';
  38. public $matchgrades = 'error';
  39. public $catfromfile = 0;
  40. public $contextfromfile = 0;
  41. public $cattofile = 0;
  42. public $contexttofile = 0;
  43. public $questionids = array();
  44. public $importerrors = 0;
  45. public $stoponerror = true;
  46. public $translator = null;
  47. public $canaccessbackupdata = true;
  48. protected $importcontext = null;
  49. // functions to indicate import/export functionality
  50. // override to return true if implemented
  51. /** @return bool whether this plugin provides import functionality. */
  52. public function provide_import() {
  53. return false;
  54. }
  55. /** @return bool whether this plugin provides export functionality. */
  56. public function provide_export() {
  57. return false;
  58. }
  59. /** The string mime-type of the files that this plugin reads or writes. */
  60. public function mime_type() {
  61. return mimeinfo('type', $this->export_file_extension());
  62. }
  63. /**
  64. * @return string the file extension (including .) that is normally used for
  65. * files handled by this plugin.
  66. */
  67. public function export_file_extension() {
  68. return '.txt';
  69. }
  70. /**
  71. * Check if the given file is capable of being imported by this plugin.
  72. *
  73. * Note that expensive or detailed integrity checks on the file should
  74. * not be performed by this method. Simple file type or magic-number tests
  75. * would be suitable.
  76. *
  77. * @param stored_file $file the file to check
  78. * @return bool whether this plugin can import the file
  79. */
  80. public function can_import_file($file) {
  81. return ($file->get_mimetype() == $this->mime_type());
  82. }
  83. // Accessor methods
  84. /**
  85. * set the category
  86. * @param object category the category object
  87. */
  88. public function setCategory($category) {
  89. if (count($this->questions)) {
  90. debugging('You shouldn\'t call setCategory after setQuestions');
  91. }
  92. $this->category = $category;
  93. $this->importcontext = context::instance_by_id($this->category->contextid);
  94. }
  95. /**
  96. * Set the specific questions to export. Should not include questions with
  97. * parents (sub questions of cloze question type).
  98. * Only used for question export.
  99. * @param array of question objects
  100. */
  101. public function setQuestions($questions) {
  102. if ($this->category !== null) {
  103. debugging('You shouldn\'t call setQuestions after setCategory');
  104. }
  105. $this->questions = $questions;
  106. }
  107. /**
  108. * set the course class variable
  109. * @param course object Moodle course variable
  110. */
  111. public function setCourse($course) {
  112. $this->course = $course;
  113. }
  114. /**
  115. * set an array of contexts.
  116. * @param array $contexts Moodle course variable
  117. */
  118. public function setContexts($contexts) {
  119. $this->contexts = $contexts;
  120. $this->translator = new context_to_string_translator($this->contexts);
  121. }
  122. /**
  123. * set the filename
  124. * @param string filename name of file to import/export
  125. */
  126. public function setFilename($filename) {
  127. $this->filename = $filename;
  128. }
  129. /**
  130. * set the "real" filename
  131. * (this is what the user typed, regardless of wha happened next)
  132. * @param string realfilename name of file as typed by user
  133. */
  134. public function setRealfilename($realfilename) {
  135. $this->realfilename = $realfilename;
  136. }
  137. /**
  138. * set matchgrades
  139. * @param string matchgrades error or nearest for grades
  140. */
  141. public function setMatchgrades($matchgrades) {
  142. $this->matchgrades = $matchgrades;
  143. }
  144. /**
  145. * set catfromfile
  146. * @param bool catfromfile allow categories embedded in import file
  147. */
  148. public function setCatfromfile($catfromfile) {
  149. $this->catfromfile = $catfromfile;
  150. }
  151. /**
  152. * set contextfromfile
  153. * @param bool $contextfromfile allow contexts embedded in import file
  154. */
  155. public function setContextfromfile($contextfromfile) {
  156. $this->contextfromfile = $contextfromfile;
  157. }
  158. /**
  159. * set cattofile
  160. * @param bool cattofile exports categories within export file
  161. */
  162. public function setCattofile($cattofile) {
  163. $this->cattofile = $cattofile;
  164. }
  165. /**
  166. * set contexttofile
  167. * @param bool cattofile exports categories within export file
  168. */
  169. public function setContexttofile($contexttofile) {
  170. $this->contexttofile = $contexttofile;
  171. }
  172. /**
  173. * set stoponerror
  174. * @param bool stoponerror stops database write if any errors reported
  175. */
  176. public function setStoponerror($stoponerror) {
  177. $this->stoponerror = $stoponerror;
  178. }
  179. /**
  180. * @param bool $canaccess Whether the current use can access the backup data folder. Determines
  181. * where export files are saved.
  182. */
  183. public function set_can_access_backupdata($canaccess) {
  184. $this->canaccessbackupdata = $canaccess;
  185. }
  186. /***********************
  187. * IMPORTING FUNCTIONS
  188. ***********************/
  189. /**
  190. * Handle parsing error
  191. */
  192. protected function error($message, $text='', $questionname='') {
  193. $importerrorquestion = get_string('importerrorquestion', 'question');
  194. echo "<div class=\"importerror\">\n";
  195. echo "<strong>{$importerrorquestion} {$questionname}</strong>";
  196. if (!empty($text)) {
  197. $text = s($text);
  198. echo "<blockquote>{$text}</blockquote>\n";
  199. }
  200. echo "<strong>{$message}</strong>\n";
  201. echo "</div>";
  202. $this->importerrors++;
  203. }
  204. /**
  205. * Import for questiontype plugins
  206. * Do not override.
  207. * @param data mixed The segment of data containing the question
  208. * @param question object processed (so far) by standard import code if appropriate
  209. * @param extra mixed any additional format specific data that may be passed by the format
  210. * @param qtypehint hint about a question type from format
  211. * @return object question object suitable for save_options() or false if cannot handle
  212. */
  213. public function try_importing_using_qtypes($data, $question = null, $extra = null,
  214. $qtypehint = '') {
  215. // work out what format we are using
  216. $formatname = substr(get_class($this), strlen('qformat_'));
  217. $methodname = "import_from_{$formatname}";
  218. //first try importing using a hint from format
  219. if (!empty($qtypehint)) {
  220. $qtype = question_bank::get_qtype($qtypehint, false);
  221. if (is_object($qtype) && method_exists($qtype, $methodname)) {
  222. $question = $qtype->$methodname($data, $question, $this, $extra);
  223. if ($question) {
  224. return $question;
  225. }
  226. }
  227. }
  228. // loop through installed questiontypes checking for
  229. // function to handle this question
  230. foreach (question_bank::get_all_qtypes() as $qtype) {
  231. if (method_exists($qtype, $methodname)) {
  232. if ($question = $qtype->$methodname($data, $question, $this, $extra)) {
  233. return $question;
  234. }
  235. }
  236. }
  237. return false;
  238. }
  239. /**
  240. * Perform any required pre-processing
  241. * @return bool success
  242. */
  243. public function importpreprocess() {
  244. return true;
  245. }
  246. /**
  247. * Process the file
  248. * This method should not normally be overidden
  249. * @param object $category
  250. * @return bool success
  251. */
  252. public function importprocess($category) {
  253. global $USER, $CFG, $DB, $OUTPUT;
  254. // Raise time and memory, as importing can be quite intensive.
  255. core_php_time_limit::raise();
  256. raise_memory_limit(MEMORY_EXTRA);
  257. // STAGE 1: Parse the file
  258. echo $OUTPUT->notification(get_string('parsingquestions', 'question'), 'notifysuccess');
  259. if (! $lines = $this->readdata($this->filename)) {
  260. echo $OUTPUT->notification(get_string('cannotread', 'question'));
  261. return false;
  262. }
  263. if (!$questions = $this->readquestions($lines)) { // Extract all the questions
  264. echo $OUTPUT->notification(get_string('noquestionsinfile', 'question'));
  265. return false;
  266. }
  267. // STAGE 2: Write data to database
  268. echo $OUTPUT->notification(get_string('importingquestions', 'question',
  269. $this->count_questions($questions)), 'notifysuccess');
  270. // check for errors before we continue
  271. if ($this->stoponerror and ($this->importerrors>0)) {
  272. echo $OUTPUT->notification(get_string('importparseerror', 'question'));
  273. return true;
  274. }
  275. // get list of valid answer grades
  276. $gradeoptionsfull = question_bank::fraction_options_full();
  277. // check answer grades are valid
  278. // (now need to do this here because of 'stop on error': MDL-10689)
  279. $gradeerrors = 0;
  280. $goodquestions = array();
  281. foreach ($questions as $question) {
  282. if (!empty($question->fraction) and (is_array($question->fraction))) {
  283. $fractions = $question->fraction;
  284. $invalidfractions = array();
  285. foreach ($fractions as $key => $fraction) {
  286. $newfraction = match_grade_options($gradeoptionsfull, $fraction,
  287. $this->matchgrades);
  288. if ($newfraction === false) {
  289. $invalidfractions[] = $fraction;
  290. } else {
  291. $fractions[$key] = $newfraction;
  292. }
  293. }
  294. if ($invalidfractions) {
  295. echo $OUTPUT->notification(get_string('invalidgrade', 'question',
  296. implode(', ', $invalidfractions)));
  297. ++$gradeerrors;
  298. continue;
  299. } else {
  300. $question->fraction = $fractions;
  301. }
  302. }
  303. $goodquestions[] = $question;
  304. }
  305. $questions = $goodquestions;
  306. // check for errors before we continue
  307. if ($this->stoponerror && $gradeerrors > 0) {
  308. return false;
  309. }
  310. // count number of questions processed
  311. $count = 0;
  312. foreach ($questions as $question) { // Process and store each question
  313. $transaction = $DB->start_delegated_transaction();
  314. // reset the php timeout
  315. core_php_time_limit::raise();
  316. // check for category modifiers
  317. if ($question->qtype == 'category') {
  318. if ($this->catfromfile) {
  319. // find/create category object
  320. $catpath = $question->category;
  321. $newcategory = $this->create_category_path($catpath);
  322. if (!empty($newcategory)) {
  323. $this->category = $newcategory;
  324. }
  325. }
  326. $transaction->allow_commit();
  327. continue;
  328. }
  329. $question->context = $this->importcontext;
  330. $count++;
  331. echo "<hr /><p><b>{$count}</b>. ".$this->format_question_text($question)."</p>";
  332. $question->category = $this->category->id;
  333. $question->stamp = make_unique_id_code(); // Set the unique code (not to be changed)
  334. $question->createdby = $USER->id;
  335. $question->timecreated = time();
  336. $question->modifiedby = $USER->id;
  337. $question->timemodified = time();
  338. $fileoptions = array(
  339. 'subdirs' => true,
  340. 'maxfiles' => -1,
  341. 'maxbytes' => 0,
  342. );
  343. $question->id = $DB->insert_record('question', $question);
  344. if (isset($question->questiontextitemid)) {
  345. $question->questiontext = file_save_draft_area_files($question->questiontextitemid,
  346. $this->importcontext->id, 'question', 'questiontext', $question->id,
  347. $fileoptions, $question->questiontext);
  348. } else if (isset($question->questiontextfiles)) {
  349. foreach ($question->questiontextfiles as $file) {
  350. question_bank::get_qtype($question->qtype)->import_file(
  351. $this->importcontext, 'question', 'questiontext', $question->id, $file);
  352. }
  353. }
  354. if (isset($question->generalfeedbackitemid)) {
  355. $question->generalfeedback = file_save_draft_area_files($question->generalfeedbackitemid,
  356. $this->importcontext->id, 'question', 'generalfeedback', $question->id,
  357. $fileoptions, $question->generalfeedback);
  358. } else if (isset($question->generalfeedbackfiles)) {
  359. foreach ($question->generalfeedbackfiles as $file) {
  360. question_bank::get_qtype($question->qtype)->import_file(
  361. $this->importcontext, 'question', 'generalfeedback', $question->id, $file);
  362. }
  363. }
  364. $DB->update_record('question', $question);
  365. $this->questionids[] = $question->id;
  366. // Now to save all the answers and type-specific options
  367. $result = question_bank::get_qtype($question->qtype)->save_question_options($question);
  368. if (isset($question->tags)) {
  369. core_tag_tag::set_item_tags('core_question', 'question', $question->id, $question->context, $question->tags);
  370. }
  371. if (!empty($result->error)) {
  372. echo $OUTPUT->notification($result->error);
  373. // Can't use $transaction->rollback(); since it requires an exception,
  374. // and I don't want to rewrite this code to change the error handling now.
  375. $DB->force_transaction_rollback();
  376. return false;
  377. }
  378. $transaction->allow_commit();
  379. if (!empty($result->notice)) {
  380. echo $OUTPUT->notification($result->notice);
  381. return true;
  382. }
  383. // Give the question a unique version stamp determined by question_hash()
  384. $DB->set_field('question', 'version', question_hash($question),
  385. array('id' => $question->id));
  386. }
  387. return true;
  388. }
  389. /**
  390. * Count all non-category questions in the questions array.
  391. *
  392. * @param array questions An array of question objects.
  393. * @return int The count.
  394. *
  395. */
  396. protected function count_questions($questions) {
  397. $count = 0;
  398. if (!is_array($questions)) {
  399. return $count;
  400. }
  401. foreach ($questions as $question) {
  402. if (!is_object($question) || !isset($question->qtype) ||
  403. ($question->qtype == 'category')) {
  404. continue;
  405. }
  406. $count++;
  407. }
  408. return $count;
  409. }
  410. /**
  411. * find and/or create the category described by a delimited list
  412. * e.g. $course$/tom/dick/harry or tom/dick/harry
  413. *
  414. * removes any context string no matter whether $getcontext is set
  415. * but if $getcontext is set then ignore the context and use selected category context.
  416. *
  417. * @param string catpath delimited category path
  418. * @param int courseid course to search for categories
  419. * @return mixed category object or null if fails
  420. */
  421. protected function create_category_path($catpath) {
  422. global $DB;
  423. $catnames = $this->split_category_path($catpath);
  424. $parent = 0;
  425. $category = null;
  426. // check for context id in path, it might not be there in pre 1.9 exports
  427. $matchcount = preg_match('/^\$([a-z]+)\$$/', $catnames[0], $matches);
  428. if ($matchcount == 1) {
  429. $contextid = $this->translator->string_to_context($matches[1]);
  430. array_shift($catnames);
  431. } else {
  432. $contextid = false;
  433. }
  434. if ($this->contextfromfile && $contextid !== false) {
  435. $context = context::instance_by_id($contextid);
  436. require_capability('moodle/question:add', $context);
  437. } else {
  438. $context = context::instance_by_id($this->category->contextid);
  439. }
  440. $this->importcontext = $context;
  441. // Now create any categories that need to be created.
  442. foreach ($catnames as $catname) {
  443. if ($category = $DB->get_record('question_categories',
  444. array('name' => $catname, 'contextid' => $context->id, 'parent' => $parent))) {
  445. $parent = $category->id;
  446. } else {
  447. require_capability('moodle/question:managecategory', $context);
  448. // create the new category
  449. $category = new stdClass();
  450. $category->contextid = $context->id;
  451. $category->name = $catname;
  452. $category->info = '';
  453. $category->parent = $parent;
  454. $category->sortorder = 999;
  455. $category->stamp = make_unique_id_code();
  456. $id = $DB->insert_record('question_categories', $category);
  457. $category->id = $id;
  458. $parent = $id;
  459. }
  460. }
  461. return $category;
  462. }
  463. /**
  464. * Return complete file within an array, one item per line
  465. * @param string filename name of file
  466. * @return mixed contents array or false on failure
  467. */
  468. protected function readdata($filename) {
  469. if (is_readable($filename)) {
  470. $filearray = file($filename);
  471. // If the first line of the file starts with a UTF-8 BOM, remove it.
  472. $filearray[0] = core_text::trim_utf8_bom($filearray[0]);
  473. // Check for Macintosh OS line returns (ie file on one line), and fix.
  474. if (preg_match("~\r~", $filearray[0]) AND !preg_match("~\n~", $filearray[0])) {
  475. return explode("\r", $filearray[0]);
  476. } else {
  477. return $filearray;
  478. }
  479. }
  480. return false;
  481. }
  482. /**
  483. * Parses an array of lines into an array of questions,
  484. * where each item is a question object as defined by
  485. * readquestion(). Questions are defined as anything
  486. * between blank lines.
  487. *
  488. * NOTE this method used to take $context as a second argument. However, at
  489. * the point where this method was called, it was impossible to know what
  490. * context the quetsions were going to be saved into, so the value could be
  491. * wrong. Also, none of the standard question formats were using this argument,
  492. * so it was removed. See MDL-32220.
  493. *
  494. * If your format does not use blank lines as a delimiter
  495. * then you will need to override this method. Even then
  496. * try to use readquestion for each question
  497. * @param array lines array of lines from readdata
  498. * @return array array of question objects
  499. */
  500. protected function readquestions($lines) {
  501. $questions = array();
  502. $currentquestion = array();
  503. foreach ($lines as $line) {
  504. $line = trim($line);
  505. if (empty($line)) {
  506. if (!empty($currentquestion)) {
  507. if ($question = $this->readquestion($currentquestion)) {
  508. $questions[] = $question;
  509. }
  510. $currentquestion = array();
  511. }
  512. } else {
  513. $currentquestion[] = $line;
  514. }
  515. }
  516. if (!empty($currentquestion)) { // There may be a final question
  517. if ($question = $this->readquestion($currentquestion)) {
  518. $questions[] = $question;
  519. }
  520. }
  521. return $questions;
  522. }
  523. /**
  524. * return an "empty" question
  525. * Somewhere to specify question parameters that are not handled
  526. * by import but are required db fields.
  527. * This should not be overridden.
  528. * @return object default question
  529. */
  530. protected function defaultquestion() {
  531. global $CFG;
  532. static $defaultshuffleanswers = null;
  533. if (is_null($defaultshuffleanswers)) {
  534. $defaultshuffleanswers = get_config('quiz', 'shuffleanswers');
  535. }
  536. $question = new stdClass();
  537. $question->shuffleanswers = $defaultshuffleanswers;
  538. $question->defaultmark = 1;
  539. $question->image = "";
  540. $question->usecase = 0;
  541. $question->multiplier = array();
  542. $question->questiontextformat = FORMAT_MOODLE;
  543. $question->generalfeedback = '';
  544. $question->generalfeedbackformat = FORMAT_MOODLE;
  545. $question->correctfeedback = '';
  546. $question->partiallycorrectfeedback = '';
  547. $question->incorrectfeedback = '';
  548. $question->answernumbering = 'abc';
  549. $question->penalty = 0.3333333;
  550. $question->length = 1;
  551. // this option in case the questiontypes class wants
  552. // to know where the data came from
  553. $question->export_process = true;
  554. $question->import_process = true;
  555. return $question;
  556. }
  557. /**
  558. * Construct a reasonable default question name, based on the start of the question text.
  559. * @param string $questiontext the question text.
  560. * @param string $default default question name to use if the constructed one comes out blank.
  561. * @return string a reasonable question name.
  562. */
  563. public function create_default_question_name($questiontext, $default) {
  564. $name = $this->clean_question_name(shorten_text($questiontext, 80));
  565. if ($name) {
  566. return $name;
  567. } else {
  568. return $default;
  569. }
  570. }
  571. /**
  572. * Ensure that a question name does not contain anything nasty, and will fit in the DB field.
  573. * @param string $name the raw question name.
  574. * @return string a safe question name.
  575. */
  576. public function clean_question_name($name) {
  577. $name = clean_param($name, PARAM_TEXT); // Matches what the question editing form does.
  578. $name = trim($name);
  579. $trimlength = 251;
  580. while (core_text::strlen($name) > 255 && $trimlength > 0) {
  581. $name = shorten_text($name, $trimlength);
  582. $trimlength -= 10;
  583. }
  584. return $name;
  585. }
  586. /**
  587. * Add a blank combined feedback to a question object.
  588. * @param object question
  589. * @return object question
  590. */
  591. protected function add_blank_combined_feedback($question) {
  592. $question->correctfeedback['text'] = '';
  593. $question->correctfeedback['format'] = $question->questiontextformat;
  594. $question->correctfeedback['files'] = array();
  595. $question->partiallycorrectfeedback['text'] = '';
  596. $question->partiallycorrectfeedback['format'] = $question->questiontextformat;
  597. $question->partiallycorrectfeedback['files'] = array();
  598. $question->incorrectfeedback['text'] = '';
  599. $question->incorrectfeedback['format'] = $question->questiontextformat;
  600. $question->incorrectfeedback['files'] = array();
  601. return $question;
  602. }
  603. /**
  604. * Given the data known to define a question in
  605. * this format, this function converts it into a question
  606. * object suitable for processing and insertion into Moodle.
  607. *
  608. * If your format does not use blank lines to delimit questions
  609. * (e.g. an XML format) you must override 'readquestions' too
  610. * @param $lines mixed data that represents question
  611. * @return object question object
  612. */
  613. protected function readquestion($lines) {
  614. // We should never get there unless the qformat plugin is broken.
  615. throw new coding_exception('Question format plugin is missing important code: readquestion.');
  616. return null;
  617. }
  618. /**
  619. * Override if any post-processing is required
  620. * @return bool success
  621. */
  622. public function importpostprocess() {
  623. return true;
  624. }
  625. /*******************
  626. * EXPORT FUNCTIONS
  627. *******************/
  628. /**
  629. * Provide export functionality for plugin questiontypes
  630. * Do not override
  631. * @param name questiontype name
  632. * @param question object data to export
  633. * @param extra mixed any addition format specific data needed
  634. * @return string the data to append to export or false if error (or unhandled)
  635. */
  636. protected function try_exporting_using_qtypes($name, $question, $extra=null) {
  637. // work out the name of format in use
  638. $formatname = substr(get_class($this), strlen('qformat_'));
  639. $methodname = "export_to_{$formatname}";
  640. $qtype = question_bank::get_qtype($name, false);
  641. if (method_exists($qtype, $methodname)) {
  642. return $qtype->$methodname($question, $this, $extra);
  643. }
  644. return false;
  645. }
  646. /**
  647. * Do any pre-processing that may be required
  648. * @param bool success
  649. */
  650. public function exportpreprocess() {
  651. return true;
  652. }
  653. /**
  654. * Enable any processing to be done on the content
  655. * just prior to the file being saved
  656. * default is to do nothing
  657. * @param string output text
  658. * @param string processed output text
  659. */
  660. protected function presave_process($content) {
  661. return $content;
  662. }
  663. /**
  664. * Do the export
  665. * For most types this should not need to be overrided
  666. * @return stored_file
  667. */
  668. public function exportprocess() {
  669. global $CFG, $OUTPUT, $DB, $USER;
  670. // get the questions (from database) in this category
  671. // only get q's with no parents (no cloze subquestions specifically)
  672. if ($this->category) {
  673. $questions = get_questions_category($this->category, true);
  674. } else {
  675. $questions = $this->questions;
  676. }
  677. $count = 0;
  678. // results are first written into string (and then to a file)
  679. // so create/initialize the string here
  680. $expout = "";
  681. // track which category questions are in
  682. // if it changes we will record the category change in the output
  683. // file if selected. 0 means that it will get printed before the 1st question
  684. $trackcategory = 0;
  685. // iterate through questions
  686. foreach ($questions as $question) {
  687. // used by file api
  688. $contextid = $DB->get_field('question_categories', 'contextid',
  689. array('id' => $question->category));
  690. $question->contextid = $contextid;
  691. // do not export hidden questions
  692. if (!empty($question->hidden)) {
  693. continue;
  694. }
  695. // do not export random questions
  696. if ($question->qtype == 'random') {
  697. continue;
  698. }
  699. // check if we need to record category change
  700. if ($this->cattofile) {
  701. if ($question->category != $trackcategory) {
  702. $trackcategory = $question->category;
  703. $categoryname = $this->get_category_path($trackcategory, $this->contexttofile);
  704. // create 'dummy' question for category export
  705. $dummyquestion = new stdClass();
  706. $dummyquestion->qtype = 'category';
  707. $dummyquestion->category = $categoryname;
  708. $dummyquestion->name = 'Switch category to ' . $categoryname;
  709. $dummyquestion->id = 0;
  710. $dummyquestion->questiontextformat = '';
  711. $dummyquestion->contextid = 0;
  712. $expout .= $this->writequestion($dummyquestion) . "\n";
  713. }
  714. }
  715. // export the question displaying message
  716. $count++;
  717. if (question_has_capability_on($question, 'view', $question->category)) {
  718. $expout .= $this->writequestion($question, $contextid) . "\n";
  719. }
  720. }
  721. // continue path for following error checks
  722. $course = $this->course;
  723. $continuepath = "{$CFG->wwwroot}/question/export.php?courseid={$course->id}";
  724. // did we actually process anything
  725. if ($count==0) {
  726. print_error('noquestions', 'question', $continuepath);
  727. }
  728. // final pre-process on exported data
  729. $expout = $this->presave_process($expout);
  730. return $expout;
  731. }
  732. /**
  733. * get the category as a path (e.g., tom/dick/harry)
  734. * @param int id the id of the most nested catgory
  735. * @return string the path
  736. */
  737. protected function get_category_path($id, $includecontext = true) {
  738. global $DB;
  739. if (!$category = $DB->get_record('question_categories', array('id' => $id))) {
  740. print_error('cannotfindcategory', 'error', '', $id);
  741. }
  742. $contextstring = $this->translator->context_to_string($category->contextid);
  743. $pathsections = array();
  744. do {
  745. $pathsections[] = $category->name;
  746. $id = $category->parent;
  747. } while ($category = $DB->get_record('question_categories', array('id' => $id)));
  748. if ($includecontext) {
  749. $pathsections[] = '$' . $contextstring . '$';
  750. }
  751. $path = $this->assemble_category_path(array_reverse($pathsections));
  752. return $path;
  753. }
  754. /**
  755. * Convert a list of category names, possibly preceeded by one of the
  756. * context tokens like $course$, into a string representation of the
  757. * category path.
  758. *
  759. * Names are separated by / delimiters. And /s in the name are replaced by //.
  760. *
  761. * To reverse the process and split the paths into names, use
  762. * {@link split_category_path()}.
  763. *
  764. * @param array $names
  765. * @return string
  766. */
  767. protected function assemble_category_path($names) {
  768. $escapednames = array();
  769. foreach ($names as $name) {
  770. $escapedname = str_replace('/', '//', $name);
  771. if (substr($escapedname, 0, 1) == '/') {
  772. $escapedname = ' ' . $escapedname;
  773. }
  774. if (substr($escapedname, -1) == '/') {
  775. $escapedname = $escapedname . ' ';
  776. }
  777. $escapednames[] = $escapedname;
  778. }
  779. return implode('/', $escapednames);
  780. }
  781. /**
  782. * Convert a string, as returned by {@link assemble_category_path()},
  783. * back into an array of category names.
  784. *
  785. * Each category name is cleaned by a call to clean_param(, PARAM_TEXT),
  786. * which matches the cleaning in question/category_form.php.
  787. *
  788. * @param string $path
  789. * @return array of category names.
  790. */
  791. protected function split_category_path($path) {
  792. $rawnames = preg_split('~(?<!/)/(?!/)~', $path);
  793. $names = array();
  794. foreach ($rawnames as $rawname) {
  795. $names[] = clean_param(trim(str_replace('//', '/', $rawname)), PARAM_TEXT);
  796. }
  797. return $names;
  798. }
  799. /**
  800. * Do an post-processing that may be required
  801. * @return bool success
  802. */
  803. protected function exportpostprocess() {
  804. return true;
  805. }
  806. /**
  807. * convert a single question object into text output in the given
  808. * format.
  809. * This must be overriden
  810. * @param object question question object
  811. * @return mixed question export text or null if not implemented
  812. */
  813. protected function writequestion($question) {
  814. // if not overidden, then this is an error.
  815. throw new coding_exception('Question format plugin is missing important code: writequestion.');
  816. return null;
  817. }
  818. /**
  819. * Convert the question text to plain text, so it can safely be displayed
  820. * during import to let the user see roughly what is going on.
  821. */
  822. protected function format_question_text($question) {
  823. return question_utils::to_plain_text($question->questiontext,
  824. $question->questiontextformat);
  825. }
  826. }
  827. class qformat_based_on_xml extends qformat_default {
  828. /**
  829. * A lot of imported files contain unwanted entities.
  830. * This method tries to clean up all known problems.
  831. * @param string str string to correct
  832. * @return string the corrected string
  833. */
  834. public function cleaninput($str) {
  835. $html_code_list = array(
  836. "&#039;" => "'",
  837. "&#8217;" => "'",
  838. "&#8220;" => "\"",
  839. "&#8221;" => "\"",
  840. "&#8211;" => "-",
  841. "&#8212;" => "-",
  842. );
  843. $str = strtr($str, $html_code_list);
  844. // Use core_text entities_to_utf8 function to convert only numerical entities.
  845. $str = core_text::entities_to_utf8($str, false);
  846. return $str;
  847. }
  848. /**
  849. * Return the array moodle is expecting
  850. * for an HTML text. No processing is done on $text.
  851. * qformat classes that want to process $text
  852. * for instance to import external images files
  853. * and recode urls in $text must overwrite this method.
  854. * @param array $text some HTML text string
  855. * @return array with keys text, format and files.
  856. */
  857. public function text_field($text) {
  858. return array(
  859. 'text' => trim($text),
  860. 'format' => FORMAT_HTML,
  861. 'files' => array(),
  862. );
  863. }
  864. /**
  865. * Return the value of a node, given a path to the node
  866. * if it doesn't exist return the default value.
  867. * @param array xml data to read
  868. * @param array path path to node expressed as array
  869. * @param mixed default
  870. * @param bool istext process as text
  871. * @param string error if set value must exist, return false and issue message if not
  872. * @return mixed value
  873. */
  874. public function getpath($xml, $path, $default, $istext=false, $error='') {
  875. foreach ($path as $index) {
  876. if (!isset($xml[$index])) {
  877. if (!empty($error)) {
  878. $this->error($error);
  879. return false;
  880. } else {
  881. return $default;
  882. }
  883. }
  884. $xml = $xml[$index];
  885. }
  886. if ($istext) {
  887. if (!is_string($xml)) {
  888. $this->error(get_string('invalidxml', 'qformat_xml'));
  889. }
  890. $xml = trim($xml);
  891. }
  892. return $xml;
  893. }
  894. }