PageRenderTime 70ms CodeModel.GetById 28ms RepoModel.GetById 0ms app.codeStats 0ms

/question/format/blackboard_six/formatqti.php

https://bitbucket.org/moodle/moodle
PHP | 914 lines | 655 code | 71 blank | 188 comment | 112 complexity | 53670dbd29d1933611b20c52f2fa6d7b MD5 | raw file
Possible License(s): Apache-2.0, LGPL-2.1, BSD-3-Clause, MIT, GPL-3.0
  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. * Blackboard V5 and V6 question importer.
  18. *
  19. * @package qformat_blackboard_six
  20. * @copyright 2005 Michael Penney
  21. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  22. */
  23. defined('MOODLE_INTERNAL') || die();
  24. require_once($CFG->libdir . '/xmlize.php');
  25. /**
  26. * Blackboard 6.0 question importer.
  27. *
  28. * @copyright 2005 Michael Penney
  29. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  30. */
  31. class qformat_blackboard_six_qti extends qformat_blackboard_six_base {
  32. /**
  33. * Parse the xml document into an array of questions
  34. * this *could* burn memory - but it won't happen that much
  35. * so fingers crossed!
  36. * @param array $text array of lines from the input file.
  37. * @return array (of objects) questions objects.
  38. */
  39. protected function readquestions($text) {
  40. // This converts xml to big nasty data structure,
  41. // the 0 means keep white space as it is.
  42. try {
  43. $xml = xmlize($text, 0, 'UTF-8', true);
  44. } catch (xml_format_exception $e) {
  45. $this->error($e->getMessage(), '');
  46. return false;
  47. }
  48. $questions = array();
  49. // Treat the assessment title as a category title.
  50. $this->process_category($xml, $questions);
  51. // First step : we are only interested in the <item> tags.
  52. $rawquestions = $this->getpath($xml,
  53. array('questestinterop', '#', 'assessment', 0, '#', 'section', 0, '#', 'item'),
  54. array(), false);
  55. // Each <item> tag contains data related to a single question.
  56. foreach ($rawquestions as $quest) {
  57. // Second step : parse each question data into the intermediate
  58. // rawquestion structure array.
  59. // Warning : rawquestions are not Moodle questions.
  60. $question = $this->create_raw_question($quest);
  61. // Third step : convert a rawquestion into a Moodle question.
  62. switch($question->qtype) {
  63. case "Matching":
  64. $this->process_matching($question, $questions);
  65. break;
  66. case "Multiple Choice":
  67. $this->process_mc($question, $questions);
  68. break;
  69. case "Essay":
  70. $this->process_essay($question, $questions);
  71. break;
  72. case "Multiple Answer":
  73. $this->process_ma($question, $questions);
  74. break;
  75. case "True/False":
  76. $this->process_tf($question, $questions);
  77. break;
  78. case 'Fill in the Blank':
  79. $this->process_fblank($question, $questions);
  80. break;
  81. case 'Short Response':
  82. $this->process_essay($question, $questions);
  83. break;
  84. default:
  85. $this->error(get_string('unknownorunhandledtype', 'question', $question->qtype));
  86. break;
  87. }
  88. }
  89. return $questions;
  90. }
  91. /**
  92. * Creates a cleaner object to deal with for processing into Moodle.
  93. * The object returned is NOT a moodle question object.
  94. * @param array $quest XML <item> question data
  95. * @return object rawquestion
  96. */
  97. public function create_raw_question($quest) {
  98. $rawquestion = new stdClass();
  99. $rawquestion->qtype = $this->getpath($quest,
  100. array('#', 'itemmetadata', 0, '#', 'bbmd_questiontype', 0, '#'),
  101. '', true);
  102. $rawquestion->id = $this->getpath($quest,
  103. array('#', 'itemmetadata', 0, '#', 'bbmd_asi_object_id', 0, '#'),
  104. '', true);
  105. $presentation = new stdClass();
  106. $presentation->blocks = $this->getpath($quest,
  107. array('#', 'presentation', 0, '#', 'flow', 0, '#', 'flow'),
  108. array(), false);
  109. foreach ($presentation->blocks as $pblock) {
  110. $block = new stdClass();
  111. $block->type = $this->getpath($pblock,
  112. array('@', 'class'),
  113. '', true);
  114. switch($block->type) {
  115. case 'QUESTION_BLOCK':
  116. $subblocks = $this->getpath($pblock,
  117. array('#', 'flow'),
  118. array(), false);
  119. foreach ($subblocks as $sblock) {
  120. $this->process_block($sblock, $block);
  121. }
  122. break;
  123. case 'RESPONSE_BLOCK':
  124. $choices = null;
  125. switch($rawquestion->qtype) {
  126. case 'Matching':
  127. $bbsubquestions = $this->getpath($pblock,
  128. array('#', 'flow'),
  129. array(), false);
  130. foreach ($bbsubquestions as $bbsubquestion) {
  131. $subquestion = new stdClass();
  132. $subquestion->ident = $this->getpath($bbsubquestion,
  133. array('#', 'response_lid', 0, '@', 'ident'),
  134. '', true);
  135. $this->process_block($this->getpath($bbsubquestion,
  136. array('#', 'flow', 0),
  137. false, false), $subquestion);
  138. $bbchoices = $this->getpath($bbsubquestion,
  139. array('#', 'response_lid', 0, '#', 'render_choice', 0,
  140. '#', 'flow_label', 0, '#', 'response_label'),
  141. array(), false);
  142. $choices = array();
  143. $this->process_choices($bbchoices, $choices);
  144. $subquestion->choices = $choices;
  145. if (!isset($block->subquestions)) {
  146. $block->subquestions = array();
  147. }
  148. $block->subquestions[] = $subquestion;
  149. }
  150. break;
  151. case 'Multiple Answer':
  152. $bbchoices = $this->getpath($pblock,
  153. array('#', 'response_lid', 0, '#', 'render_choice', 0, '#', 'flow_label'),
  154. array(), false);
  155. $choices = array();
  156. $this->process_choices($bbchoices, $choices);
  157. $block->choices = $choices;
  158. break;
  159. case 'Essay':
  160. // Doesn't apply since the user responds with text input.
  161. break;
  162. case 'Multiple Choice':
  163. $mcchoices = $this->getpath($pblock,
  164. array('#', 'response_lid', 0, '#', 'render_choice', 0, '#', 'flow_label'),
  165. array(), false);
  166. foreach ($mcchoices as $mcchoice) {
  167. $choices = new stdClass();
  168. $choices = $this->process_block($mcchoice, $choices);
  169. $block->choices[] = $choices;
  170. }
  171. break;
  172. case 'Short Response':
  173. // Do nothing?
  174. break;
  175. case 'Fill in the Blank':
  176. // Do nothing?
  177. break;
  178. default:
  179. $bbchoices = $this->getpath($pblock,
  180. array('#', 'response_lid', 0, '#', 'render_choice', 0, '#',
  181. 'flow_label', 0, '#', 'response_label'),
  182. array(), false);
  183. $choices = array();
  184. $this->process_choices($bbchoices, $choices);
  185. $block->choices = $choices;
  186. }
  187. break;
  188. case 'RIGHT_MATCH_BLOCK':
  189. $matchinganswerset = $this->getpath($pblock,
  190. array('#', 'flow'),
  191. false, false);
  192. $answerset = array();
  193. foreach ($matchinganswerset as $answer) {
  194. $bbanswer = new stdClass;
  195. $bbanswer->text = $this->getpath($answer,
  196. array('#', 'flow', 0, '#', 'material', 0, '#', 'mat_extension',
  197. 0, '#', 'mat_formattedtext', 0, '#'),
  198. false, false);
  199. $answerset[] = $bbanswer;
  200. }
  201. $block->matchinganswerset = $answerset;
  202. break;
  203. default:
  204. $this->error(get_string('unhandledpresblock', 'qformat_blackboard_six'));
  205. break;
  206. }
  207. $rawquestion->{$block->type} = $block;
  208. }
  209. // Determine response processing.
  210. // There is a section called 'outcomes' that I don't know what to do with.
  211. $resprocessing = $this->getpath($quest,
  212. array('#', 'resprocessing'),
  213. array(), false);
  214. $respconditions = $this->getpath($resprocessing[0],
  215. array('#', 'respcondition'),
  216. array(), false);
  217. $responses = array();
  218. if ($rawquestion->qtype == 'Matching') {
  219. $this->process_matching_responses($respconditions, $responses);
  220. } else {
  221. $this->process_responses($respconditions, $responses);
  222. }
  223. $rawquestion->responses = $responses;
  224. $feedbackset = $this->getpath($quest,
  225. array('#', 'itemfeedback'),
  226. array(), false);
  227. $feedbacks = array();
  228. $this->process_feedback($feedbackset, $feedbacks);
  229. $rawquestion->feedback = $feedbacks;
  230. return $rawquestion;
  231. }
  232. /**
  233. * Helper function to process an XML block into an object.
  234. * Can call himself recursively if necessary to parse this branch of the XML tree.
  235. * @param array $curblock XML block to parse
  236. * @param object $block block already parsed so far
  237. * @return object $block parsed
  238. */
  239. public function process_block($curblock, $block) {
  240. $curtype = $this->getpath($curblock,
  241. array('@', 'class'),
  242. '', true);
  243. switch($curtype) {
  244. case 'FORMATTED_TEXT_BLOCK':
  245. $text = $this->getpath($curblock,
  246. array('#', 'material', 0, '#', 'mat_extension', 0, '#', 'mat_formattedtext', 0, '#'),
  247. '', true);
  248. $block->text = $this->strip_applet_tags_get_mathml($text);
  249. break;
  250. case 'FILE_BLOCK':
  251. $block->filename = $this->getpath($curblock,
  252. array('#', 'material', 0, '#'),
  253. '', true);
  254. if ($block->filename != '') {
  255. // TODO : determine what to do with the file's content.
  256. $this->error(get_string('filenothandled', 'qformat_blackboard_six', $block->filename));
  257. }
  258. break;
  259. case 'Block':
  260. if ($this->getpath($curblock,
  261. array('#', 'material', 0, '#', 'mattext'),
  262. false, false)) {
  263. $block->text = $this->getpath($curblock,
  264. array('#', 'material', 0, '#', 'mattext', 0, '#'),
  265. '', true);
  266. } else if ($this->getpath($curblock,
  267. array('#', 'material', 0, '#', 'mat_extension', 0, '#', 'mat_formattedtext'),
  268. false, false)) {
  269. $block->text = $this->getpath($curblock,
  270. array('#', 'material', 0, '#', 'mat_extension', 0, '#', 'mat_formattedtext', 0, '#'),
  271. '', true);
  272. } else if ($this->getpath($curblock,
  273. array('#', 'response_label'),
  274. false, false)) {
  275. // This is a response label block.
  276. $subblocks = $this->getpath($curblock,
  277. array('#', 'response_label', 0),
  278. array(), false);
  279. if (!isset($block->ident)) {
  280. if ($this->getpath($subblocks,
  281. array('@', 'ident'), '', true)) {
  282. $block->ident = $this->getpath($subblocks,
  283. array('@', 'ident'), '', true);
  284. }
  285. }
  286. foreach ($this->getpath($subblocks,
  287. array('#', 'flow_mat'), array(), false) as $subblock) {
  288. $this->process_block($subblock, $block);
  289. }
  290. } else {
  291. if ($this->getpath($curblock,
  292. array('#', 'flow_mat'), false, false)
  293. || $this->getpath($curblock,
  294. array('#', 'flow'), false, false)) {
  295. if ($this->getpath($curblock,
  296. array('#', 'flow_mat'), false, false)) {
  297. $subblocks = $this->getpath($curblock,
  298. array('#', 'flow_mat'), array(), false);
  299. } else if ($this->getpath($curblock,
  300. array('#', 'flow'), false, false)) {
  301. $subblocks = $this->getpath($curblock,
  302. array('#', 'flow'), array(), false);
  303. }
  304. foreach ($subblocks as $sblock) {
  305. // This will recursively grab the sub blocks which should be of one of the other types.
  306. $this->process_block($sblock, $block);
  307. }
  308. }
  309. }
  310. break;
  311. case 'LINK_BLOCK':
  312. // Not sure how this should be included?
  313. $link = $this->getpath($curblock,
  314. array('#', 'material', 0, '#', 'mattext', 0, '@', 'uri'), '', true);
  315. if (!empty($link)) {
  316. $block->link = $link;
  317. } else {
  318. $block->link = '';
  319. }
  320. break;
  321. }
  322. return $block;
  323. }
  324. /**
  325. * Preprocess XML blocks containing data for questions' choices.
  326. * Called by {@link create_raw_question()}
  327. * for matching, multichoice and fill in the blank questions.
  328. * @param array $bbchoices XML block to parse
  329. * @param array $choices array of choices suitable for a rawquestion.
  330. */
  331. protected function process_choices($bbchoices, &$choices) {
  332. foreach ($bbchoices as $choice) {
  333. if ($this->getpath($choice,
  334. array('@', 'ident'), '', true)) {
  335. $curchoice = $this->getpath($choice,
  336. array('@', 'ident'), '', true);
  337. } else { // For multiple answers.
  338. $curchoice = $this->getpath($choice,
  339. array('#', 'response_label', 0), array(), false);
  340. }
  341. if ($this->getpath($choice,
  342. array('#', 'flow_mat', 0), false, false)) { // For multiple answers.
  343. $curblock = $this->getpath($choice,
  344. array('#', 'flow_mat', 0), false, false);
  345. // Reset $curchoice to new stdClass because process_block is expecting an object
  346. // for the second argument and not a string,
  347. // which is what is was set as originally - CT 8/7/06.
  348. $curchoice = new stdClass();
  349. $this->process_block($curblock, $curchoice);
  350. } else if ($this->getpath($choice,
  351. array('#', 'response_label'), false, false)) {
  352. // Reset $curchoice to new stdClass because process_block is expecting an object
  353. // for the second argument and not a string,
  354. // which is what is was set as originally - CT 8/7/06.
  355. $curchoice = new stdClass();
  356. $this->process_block($choice, $curchoice);
  357. }
  358. $choices[] = $curchoice;
  359. }
  360. }
  361. /**
  362. * Preprocess XML blocks containing data for subanswers
  363. * Called by {@link create_raw_question()}
  364. * for matching questions only.
  365. * @param array $bbresponses XML block to parse
  366. * @param array $responses array of responses suitable for a matching rawquestion.
  367. */
  368. protected function process_matching_responses($bbresponses, &$responses) {
  369. foreach ($bbresponses as $bbresponse) {
  370. $response = new stdClass;
  371. if ($this->getpath($bbresponse,
  372. array('#', 'conditionvar', 0, '#', 'varequal'), false, false)) {
  373. $response->correct = $this->getpath($bbresponse,
  374. array('#', 'conditionvar', 0, '#', 'varequal', 0, '#'), '', true);
  375. $response->ident = $this->getpath($bbresponse,
  376. array('#', 'conditionvar', 0, '#', 'varequal', 0, '@', 'respident'), '', true);
  377. }
  378. // Suppressed an else block because if the above if condition is false,
  379. // the question is not necessary a broken one, most of the time it's an <other> tag.
  380. $response->feedback = $this->getpath($bbresponse,
  381. array('#', 'displayfeedback', 0, '@', 'linkrefid'), '', true);
  382. $responses[] = $response;
  383. }
  384. }
  385. /**
  386. * Preprocess XML blocks containing data for responses processing.
  387. * Called by {@link create_raw_question()}
  388. * for all questions types.
  389. * @param array $bbresponses XML block to parse
  390. * @param array $responses array of responses suitable for a rawquestion.
  391. */
  392. protected function process_responses($bbresponses, &$responses) {
  393. foreach ($bbresponses as $bbresponse) {
  394. $response = new stdClass();
  395. if ($this->getpath($bbresponse,
  396. array('@', 'title'), '', true)) {
  397. $response->title = $this->getpath($bbresponse,
  398. array('@', 'title'), '', true);
  399. } else {
  400. $response->title = $this->getpath($bbresponse,
  401. array('#', 'displayfeedback', 0, '@', 'linkrefid'), '', true);
  402. }
  403. $response->ident = array();
  404. if ($this->getpath($bbresponse,
  405. array('#', 'conditionvar', 0, '#'), false, false)) {
  406. $response->ident[0] = $this->getpath($bbresponse,
  407. array('#', 'conditionvar', 0, '#'), array(), false);
  408. } else if ($this->getpath($bbresponse,
  409. array('#', 'conditionvar', 0, '#', 'other', 0, '#'), false, false)) {
  410. $response->ident[0] = $this->getpath($bbresponse,
  411. array('#', 'conditionvar', 0, '#', 'other', 0, '#'), array(), false);
  412. }
  413. if ($this->getpath($bbresponse,
  414. array('#', 'conditionvar', 0, '#', 'and'), false, false)) {
  415. $responseset = $this->getpath($bbresponse,
  416. array('#', 'conditionvar', 0, '#', 'and'), array(), false);
  417. foreach ($responseset as $rs) {
  418. $response->ident[] = $this->getpath($rs, array('#'), array(), false);
  419. if (!isset($response->feedback) and $this->getpath($rs, array('@'), false, false)) {
  420. $response->feedback = $this->getpath($rs,
  421. array('@', 'respident'), '', true);
  422. }
  423. }
  424. } else {
  425. $response->feedback = $this->getpath($bbresponse,
  426. array('#', 'displayfeedback', 0, '@', 'linkrefid'), '', true);
  427. }
  428. // Determine what fraction to give response.
  429. if ($this->getpath($bbresponse,
  430. array('#', 'setvar'), false, false)) {
  431. switch ($this->getpath($bbresponse,
  432. array('#', 'setvar', 0, '#'), false, false)) {
  433. case "SCORE.max":
  434. $response->fraction = 1;
  435. break;
  436. default:
  437. // I have only seen this being 0 or unset.
  438. // There are probably fractional values of SCORE.max, but I'm not sure what they look like.
  439. $response->fraction = 0;
  440. break;
  441. }
  442. } else {
  443. // Just going to assume this is the case this is probably not correct.
  444. $response->fraction = 0;
  445. }
  446. $responses[] = $response;
  447. }
  448. }
  449. /**
  450. * Preprocess XML blocks containing data for responses feedbacks.
  451. * Called by {@link create_raw_question()}
  452. * for all questions types.
  453. * @param array $feedbackset XML block to parse
  454. * @param array $feedbacks array of feedbacks suitable for a rawquestion.
  455. */
  456. public function process_feedback($feedbackset, &$feedbacks) {
  457. foreach ($feedbackset as $bbfeedback) {
  458. $feedback = new stdClass();
  459. $feedback->ident = $this->getpath($bbfeedback,
  460. array('@', 'ident'), '', true);
  461. $feedback->text = '';
  462. if ($this->getpath($bbfeedback,
  463. array('#', 'flow_mat', 0), false, false)) {
  464. $this->process_block($this->getpath($bbfeedback,
  465. array('#', 'flow_mat', 0), false, false), $feedback);
  466. } else if ($this->getpath($bbfeedback,
  467. array('#', 'solution', 0, '#', 'solutionmaterial', 0, '#', 'flow_mat', 0), false, false)) {
  468. $this->process_block($this->getpath($bbfeedback,
  469. array('#', 'solution', 0, '#', 'solutionmaterial', 0, '#', 'flow_mat', 0), false, false), $feedback);
  470. }
  471. $feedbacks[$feedback->ident] = $feedback;
  472. }
  473. }
  474. /**
  475. * Create common parts of question
  476. * @param object $quest rawquestion
  477. * @return object Moodle question.
  478. */
  479. public function process_common($quest) {
  480. $question = $this->defaultquestion();
  481. $text = $quest->QUESTION_BLOCK->text;
  482. $questiontext = $this->cleaned_text_field($text);
  483. $question->questiontext = $questiontext['text'];
  484. $question->questiontextformat = $questiontext['format']; // Needed because add_blank_combined_feedback uses it.
  485. if (isset($questiontext['itemid'])) {
  486. $question->questiontextitemid = $questiontext['itemid'];
  487. }
  488. $question->name = $this->create_default_question_name($question->questiontext,
  489. get_string('defaultname', 'qformat_blackboard_six' , $quest->id));
  490. $question->generalfeedback = '';
  491. $question->generalfeedbackformat = FORMAT_HTML;
  492. $question->generalfeedbackfiles = array();
  493. return $question;
  494. }
  495. /**
  496. * Process True / False Questions
  497. * Parse a truefalse rawquestion and add the result
  498. * to the array of questions already parsed.
  499. * @param object $quest rawquestion
  500. * @param array $questions array of Moodle questions already done
  501. */
  502. protected function process_tf($quest, &$questions) {
  503. $question = $this->process_common($quest);
  504. $question->qtype = 'truefalse';
  505. $question->single = 1; // Only one answer is allowed.
  506. $question->penalty = 1; // Penalty = 1 for truefalse questions.
  507. // 0th [response] is the correct answer.
  508. $responses = $quest->responses;
  509. $correctresponse = $this->getpath($responses[0]->ident[0],
  510. array('varequal', 0, '#'), '', true);
  511. if ($correctresponse != 'false') {
  512. $correct = true;
  513. } else {
  514. $correct = false;
  515. }
  516. $fback = new stdClass();
  517. foreach ($quest->feedback as $fb) {
  518. $fback->{$fb->ident} = $fb->text;
  519. }
  520. if ($correct) { // True is correct.
  521. $question->answer = 1;
  522. $question->feedbacktrue = $this->cleaned_text_field($fback->correct);
  523. $question->feedbackfalse = $this->cleaned_text_field($fback->incorrect);
  524. } else { // False is correct.
  525. $question->answer = 0;
  526. $question->feedbacktrue = $this->cleaned_text_field($fback->incorrect);
  527. $question->feedbackfalse = $this->cleaned_text_field($fback->correct);
  528. }
  529. $question->correctanswer = $question->answer;
  530. $questions[] = $question;
  531. }
  532. /**
  533. * Process Fill in the Blank Questions
  534. * Parse a fillintheblank rawquestion and add the result
  535. * to the array of questions already parsed.
  536. * @param object $quest rawquestion
  537. * @param array $questions array of Moodle questions already done.
  538. */
  539. protected function process_fblank($quest, &$questions) {
  540. $question = $this->process_common($quest);
  541. $question->qtype = 'shortanswer';
  542. $question->usecase = 0; // Ignore case.
  543. $answers = array();
  544. $fractions = array();
  545. $feedbacks = array();
  546. // Extract the feedback.
  547. $feedback = array();
  548. foreach ($quest->feedback as $fback) {
  549. if (isset($fback->ident)) {
  550. if ($fback->ident == 'correct' || $fback->ident == 'incorrect') {
  551. $feedback[$fback->ident] = $fback->text;
  552. }
  553. }
  554. }
  555. foreach ($quest->responses as $response) {
  556. if (isset($response->title)) {
  557. if ($this->getpath($response->ident[0],
  558. array('varequal', 0, '#'), false, false)) {
  559. // For BB Fill in the Blank, only interested in correct answers.
  560. if ($response->feedback = 'correct') {
  561. $answers[] = $this->getpath($response->ident[0],
  562. array('varequal', 0, '#'), '', true);
  563. $fractions[] = 1;
  564. if (isset($feedback['correct'])) {
  565. $feedbacks[] = $this->cleaned_text_field($feedback['correct']);
  566. } else {
  567. $feedbacks[] = $this->text_field('');
  568. }
  569. }
  570. }
  571. }
  572. }
  573. // Adding catchall to so that students can see feedback for incorrect answers when they enter something,
  574. // the instructor did not enter.
  575. $answers[] = '*';
  576. $fractions[] = 0;
  577. if (isset($feedback['incorrect'])) {
  578. $feedbacks[] = $this->cleaned_text_field($feedback['incorrect']);
  579. } else {
  580. $feedbacks[] = $this->text_field('');
  581. }
  582. $question->answer = $answers;
  583. $question->fraction = $fractions;
  584. $question->feedback = $feedbacks; // Changed to assign $feedbacks to $question->feedback instead of.
  585. if (!empty($question)) {
  586. $questions[] = $question;
  587. }
  588. }
  589. /**
  590. * Process Multichoice Questions
  591. * Parse a multichoice single answer rawquestion and add the result
  592. * to the array of questions already parsed.
  593. * @param object $quest rawquestion
  594. * @param array $questions array of Moodle questions already done.
  595. */
  596. protected function process_mc($quest, &$questions) {
  597. $question = $this->process_common($quest);
  598. $question->qtype = 'multichoice';
  599. $question = $this->add_blank_combined_feedback($question);
  600. $question->single = 1;
  601. $feedback = array();
  602. foreach ($quest->feedback as $fback) {
  603. $feedback[$fback->ident] = $fback->text;
  604. }
  605. foreach ($quest->responses as $response) {
  606. if (isset($response->title)) {
  607. if ($response->title == 'correct') {
  608. // Only one answer possible for this qtype so first index is correct answer.
  609. $correct = $this->getpath($response->ident[0],
  610. array('varequal', 0, '#'), '', true);
  611. }
  612. } else {
  613. // Fallback method for when the title is not set.
  614. if ($response->feedback == 'correct') {
  615. // Only one answer possible for this qtype so first index is correct answer.
  616. $correct = $this->getpath($response->ident[0],
  617. array('varequal', 0, '#'), '', true);
  618. }
  619. }
  620. }
  621. $i = 0;
  622. foreach ($quest->RESPONSE_BLOCK->choices as $response) {
  623. $question->answer[$i] = $this->cleaned_text_field($response->text);
  624. if ($correct == $response->ident) {
  625. $question->fraction[$i] = 1;
  626. // This is a bit of a hack to catch the feedback... first we see if a 'specific'
  627. // feedback for this response exists, then if a 'correct' feedback exists.
  628. if (!empty($feedback[$response->ident]) ) {
  629. $question->feedback[$i] = $this->cleaned_text_field($feedback[$response->ident]);
  630. } else if (!empty($feedback['correct'])) {
  631. $question->feedback[$i] = $this->cleaned_text_field($feedback['correct']);
  632. } else if (!empty($feedback[$i])) {
  633. $question->feedback[$i] = $this->cleaned_text_field($feedback[$i]);
  634. } else {
  635. $question->feedback[$i] = $this->cleaned_text_field(get_string('correct', 'question'));
  636. }
  637. } else {
  638. $question->fraction[$i] = 0;
  639. if (!empty($feedback[$response->ident]) ) {
  640. $question->feedback[$i] = $this->cleaned_text_field($feedback[$response->ident]);
  641. } else if (!empty($feedback['incorrect'])) {
  642. $question->feedback[$i] = $this->cleaned_text_field($feedback['incorrect']);
  643. } else if (!empty($feedback[$i])) {
  644. $question->feedback[$i] = $this->cleaned_text_field($feedback[$i]);
  645. } else {
  646. $question->feedback[$i] = $this->cleaned_text_field(get_string('incorrect', 'question'));
  647. }
  648. }
  649. $i++;
  650. }
  651. if (!empty($question)) {
  652. $questions[] = $question;
  653. }
  654. }
  655. /**
  656. * Process Multiple Choice Questions With Multiple Answers.
  657. * Parse a multichoice multianswer rawquestion and add the result
  658. * to the array of questions already parsed.
  659. * @param object $quest rawquestion
  660. * @param array $questions array of Moodle questions already done.
  661. */
  662. public function process_ma($quest, &$questions) {
  663. $question = $this->process_common($quest);
  664. $question->qtype = 'multichoice';
  665. $question = $this->add_blank_combined_feedback($question);
  666. $question->single = 0; // More than one answer allowed.
  667. $answers = $quest->responses;
  668. $correctanswers = array();
  669. foreach ($answers as $answer) {
  670. if ($answer->title == 'correct') {
  671. $answerset = $this->getpath($answer->ident[0],
  672. array('and', 0, '#', 'varequal'), array(), false);
  673. foreach ($answerset as $ans) {
  674. $correctanswers[] = $ans['#'];
  675. }
  676. }
  677. }
  678. $feedback = new stdClass();
  679. foreach ($quest->feedback as $fb) {
  680. $feedback->{$fb->ident} = trim($fb->text);
  681. }
  682. $correctanswercount = count($correctanswers);
  683. $fraction = 1 / $correctanswercount;
  684. $choiceset = $quest->RESPONSE_BLOCK->choices;
  685. $i = 0;
  686. foreach ($choiceset as $choice) {
  687. $question->answer[$i] = $this->cleaned_text_field(trim($choice->text));
  688. if (in_array($choice->ident, $correctanswers)) {
  689. // Correct answer.
  690. $question->fraction[$i] = $fraction;
  691. $question->feedback[$i] = $this->cleaned_text_field($feedback->correct);
  692. } else {
  693. // Wrong answer.
  694. $question->fraction[$i] = 0;
  695. $question->feedback[$i] = $this->cleaned_text_field($feedback->incorrect);
  696. }
  697. $i++;
  698. }
  699. $questions[] = $question;
  700. }
  701. /**
  702. * Process Essay Questions
  703. * Parse an essay rawquestion and add the result
  704. * to the array of questions already parsed.
  705. * @param object $quest rawquestion
  706. * @param array $questions array of Moodle questions already done.
  707. */
  708. public function process_essay($quest, &$questions) {
  709. $question = $this->process_common($quest);
  710. $question->qtype = 'essay';
  711. $question->feedback = array();
  712. // Not sure where to get the correct answer from?
  713. foreach ($quest->feedback as $feedback) {
  714. // Added this code to put the possible solution that the
  715. // instructor gives as the Moodle answer for an essay question.
  716. if ($feedback->ident == 'solution') {
  717. $question->graderinfo = $this->cleaned_text_field($feedback->text);
  718. }
  719. }
  720. // Added because essay/questiontype.php:save_question_option is expecting a
  721. // fraction property - CT 8/10/06.
  722. $question->fraction[] = 1;
  723. $question->defaultmark = 1;
  724. $question->responseformat = 'editor';
  725. $question->responserequired = 1;
  726. $question->responsefieldlines = 15;
  727. $question->attachments = 0;
  728. $question->attachmentsrequired = 0;
  729. $question->responsetemplate = $this->text_field('');
  730. $questions[] = $question;
  731. }
  732. /**
  733. * Process Matching Questions
  734. * Parse a matching rawquestion and add the result
  735. * to the array of questions already parsed.
  736. * @param object $quest rawquestion
  737. * @param array $questions array of Moodle questions already done.
  738. */
  739. public function process_matching($quest, &$questions) {
  740. // Blackboard matching questions can't be imported in core Moodle without a loss in data,
  741. // as core match question don't allow HTML in subanswers. The contributed ddmatch
  742. // question type support HTML in subanswers.
  743. // The ddmatch question type is not part of core, so we need to check if it is defined.
  744. $ddmatchisinstalled = question_bank::is_qtype_installed('ddmatch');
  745. $question = $this->process_common($quest);
  746. $question = $this->add_blank_combined_feedback($question);
  747. $question->valid = true;
  748. if ($ddmatchisinstalled) {
  749. $question->qtype = 'ddmatch';
  750. } else {
  751. $question->qtype = 'match';
  752. }
  753. // Construction of the array holding mappings between subanswers and subquestions.
  754. foreach ($quest->RESPONSE_BLOCK->subquestions as $qid => $subq) {
  755. foreach ($quest->responses as $rid => $resp) {
  756. if (isset($resp->ident) && $resp->ident == $subq->ident) {
  757. $correct = $resp->correct;
  758. }
  759. }
  760. foreach ($subq->choices as $cid => $choice) {
  761. if ($choice == $correct) {
  762. $mappings[$subq->ident] = $cid;
  763. }
  764. }
  765. }
  766. foreach ($subq->choices as $choiceid => $choice) {
  767. $subanswertext = $quest->RIGHT_MATCH_BLOCK->matchinganswerset[$choiceid]->text;
  768. if ($ddmatchisinstalled) {
  769. $subanswer = $this->cleaned_text_field($subanswertext);
  770. } else {
  771. $subanswertext = html_to_text($this->cleaninput($subanswertext), 0);
  772. $subanswer = $subanswertext;
  773. }
  774. if ($subanswertext != '') { // Only import non empty subanswers.
  775. $subquestion = '';
  776. $fiber = array_keys ($mappings, $choiceid);
  777. foreach ($fiber as $correctanswerid) {
  778. // We have found a correspondance for this subanswer so we need to take the associated subquestion.
  779. foreach ($quest->RESPONSE_BLOCK->subquestions as $qid => $subq) {
  780. $currentsubqid = $subq->ident;
  781. if (strcmp ($currentsubqid, $correctanswerid) == 0) {
  782. $subquestion = $subq->text;
  783. break;
  784. }
  785. }
  786. $question->subquestions[] = $this->cleaned_text_field($subquestion);
  787. $question->subanswers[] = $subanswer;
  788. }
  789. if ($subquestion == '') { // Then in this case, $choice is a distractor.
  790. $question->subquestions[] = $this->text_field('');
  791. $question->subanswers[] = $subanswer;
  792. }
  793. }
  794. }
  795. // Verify that this matching question has enough subquestions and subanswers.
  796. $subquestioncount = 0;
  797. $subanswercount = 0;
  798. $subanswers = $question->subanswers;
  799. foreach ($question->subquestions as $key => $subquestion) {
  800. $subquestion = $subquestion['text'];
  801. $subanswer = $subanswers[$key];
  802. if ($subquestion != '') {
  803. $subquestioncount++;
  804. }
  805. $subanswercount++;
  806. }
  807. if ($subquestioncount < 2 || $subanswercount < 3) {
  808. $this->error(get_string('notenoughtsubans', 'qformat_blackboard_six', $question->questiontext));
  809. } else {
  810. $questions[] = $question;
  811. }
  812. }
  813. /**
  814. * Add a category question entry based on the assessment title
  815. * @param array $xml the xml tree
  816. * @param array $questions the questions already parsed
  817. */
  818. public function process_category($xml, &$questions) {
  819. $title = $this->getpath($xml, array('questestinterop', '#', 'assessment', 0, '@', 'title'), '', true);
  820. $dummyquestion = new stdClass();
  821. $dummyquestion->qtype = 'category';
  822. $dummyquestion->category = $this->cleaninput($this->clean_question_name($title));
  823. $questions[] = $dummyquestion;
  824. }
  825. /**
  826. * Strip the applet tag used by Blackboard to render mathml formulas,
  827. * keeping the mathml tag.
  828. * @param string $string
  829. * @return string
  830. */
  831. public function strip_applet_tags_get_mathml($string) {
  832. if (stristr($string, '</APPLET>') === false) {
  833. return $string;
  834. } else {
  835. // Strip all applet tags keeping stuff before/after and inbetween (if mathml) them.
  836. while (stristr($string, '</APPLET>') !== false) {
  837. preg_match("/(.*)\<applet.*value=\"(\<math\>.*\<\/math\>)\".*\<\/applet\>(.*)/i", $string, $mathmls);
  838. $string = $mathmls[1].$mathmls[2].$mathmls[3];
  839. }
  840. return $string;
  841. }
  842. }
  843. }