PageRenderTime 61ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/question/format/blackboard_six/formatqti.php

https://bitbucket.org/synergylearning/campusconnect
PHP | 912 lines | 653 code | 71 blank | 188 comment | 112 complexity | ccaebeeb68c0cb71bcf278e70437a263 MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception, LGPL-3.0, GPL-3.0, LGPL-2.1, Apache-2.0, BSD-3-Clause, AGPL-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 of lines from the input file.
  37. * @param stdClass $context
  38. * @return array (of objects) questions objects.
  39. */
  40. protected function readquestions($text) {
  41. // This converts xml to big nasty data structure,
  42. // the 0 means keep white space as it is.
  43. try {
  44. $xml = xmlize($text, 0, 'UTF-8', true);
  45. } catch (xml_format_exception $e) {
  46. $this->error($e->getMessage(), '');
  47. return false;
  48. }
  49. $questions = array();
  50. // Treat the assessment title as a category title.
  51. $this->process_category($xml, $questions);
  52. // First step : we are only interested in the <item> tags.
  53. $rawquestions = $this->getpath($xml,
  54. array('questestinterop', '#', 'assessment', 0, '#', 'section', 0, '#', 'item'),
  55. array(), false);
  56. // Each <item> tag contains data related to a single question.
  57. foreach ($rawquestions as $quest) {
  58. // Second step : parse each question data into the intermediate
  59. // rawquestion structure array.
  60. // Warning : rawquestions are not Moodle questions.
  61. $question = $this->create_raw_question($quest);
  62. // Third step : convert a rawquestion into a Moodle question.
  63. switch($question->qtype) {
  64. case "Matching":
  65. $this->process_matching($question, $questions);
  66. break;
  67. case "Multiple Choice":
  68. $this->process_mc($question, $questions);
  69. break;
  70. case "Essay":
  71. $this->process_essay($question, $questions);
  72. break;
  73. case "Multiple Answer":
  74. $this->process_ma($question, $questions);
  75. break;
  76. case "True/False":
  77. $this->process_tf($question, $questions);
  78. break;
  79. case 'Fill in the Blank':
  80. $this->process_fblank($question, $questions);
  81. break;
  82. case 'Short Response':
  83. $this->process_essay($question, $questions);
  84. break;
  85. default:
  86. $this->error(get_string('unknownorunhandledtype', 'question', $question->qtype));
  87. break;
  88. }
  89. }
  90. return $questions;
  91. }
  92. /**
  93. * Creates a cleaner object to deal with for processing into Moodle.
  94. * The object returned is NOT a moodle question object.
  95. * @param array $quest XML <item> question data
  96. * @return object rawquestion
  97. */
  98. public function create_raw_question($quest) {
  99. $rawquestion = new stdClass();
  100. $rawquestion->qtype = $this->getpath($quest,
  101. array('#', 'itemmetadata', 0, '#', 'bbmd_questiontype', 0, '#'),
  102. '', true);
  103. $rawquestion->id = $this->getpath($quest,
  104. array('#', 'itemmetadata', 0, '#', 'bbmd_asi_object_id', 0, '#'),
  105. '', true);
  106. $presentation = new stdClass();
  107. $presentation->blocks = $this->getpath($quest,
  108. array('#', 'presentation', 0, '#', 'flow', 0, '#', 'flow'),
  109. array(), false);
  110. foreach ($presentation->blocks as $pblock) {
  111. $block = new stdClass();
  112. $block->type = $this->getpath($pblock,
  113. array('@', 'class'),
  114. '', true);
  115. switch($block->type) {
  116. case 'QUESTION_BLOCK':
  117. $subblocks = $this->getpath($pblock,
  118. array('#', 'flow'),
  119. array(), false);
  120. foreach ($subblocks as $sblock) {
  121. $this->process_block($sblock, $block);
  122. }
  123. break;
  124. case 'RESPONSE_BLOCK':
  125. $choices = null;
  126. switch($rawquestion->qtype) {
  127. case 'Matching':
  128. $bbsubquestions = $this->getpath($pblock,
  129. array('#', 'flow'),
  130. array(), false);
  131. foreach ($bbsubquestions as $bbsubquestion) {
  132. $subquestion = new stdClass();
  133. $subquestion->ident = $this->getpath($bbsubquestion,
  134. array('#', 'response_lid', 0, '@', 'ident'),
  135. '', true);
  136. $this->process_block($this->getpath($bbsubquestion,
  137. array('#', 'flow', 0),
  138. false, false), $subquestion);
  139. $bbchoices = $this->getpath($bbsubquestion,
  140. array('#', 'response_lid', 0, '#', 'render_choice', 0,
  141. '#', 'flow_label', 0, '#', 'response_label'),
  142. array(), false);
  143. $choices = array();
  144. $this->process_choices($bbchoices, $choices);
  145. $subquestion->choices = $choices;
  146. if (!isset($block->subquestions)) {
  147. $block->subquestions = array();
  148. }
  149. $block->subquestions[] = $subquestion;
  150. }
  151. break;
  152. case 'Multiple Answer':
  153. $bbchoices = $this->getpath($pblock,
  154. array('#', 'response_lid', 0, '#', 'render_choice', 0, '#', 'flow_label'),
  155. array(), false);
  156. $choices = array();
  157. $this->process_choices($bbchoices, $choices);
  158. $block->choices = $choices;
  159. break;
  160. case 'Essay':
  161. // Doesn't apply since the user responds with text input.
  162. break;
  163. case 'Multiple Choice':
  164. $mcchoices = $this->getpath($pblock,
  165. array('#', 'response_lid', 0, '#', 'render_choice', 0, '#', 'flow_label'),
  166. array(), false);
  167. foreach ($mcchoices as $mcchoice) {
  168. $choices = new stdClass();
  169. $choices = $this->process_block($mcchoice, $choices);
  170. $block->choices[] = $choices;
  171. }
  172. break;
  173. case 'Short Response':
  174. // Do nothing?
  175. break;
  176. case 'Fill in the Blank':
  177. // Do nothing?
  178. break;
  179. default:
  180. $bbchoices = $this->getpath($pblock,
  181. array('#', 'response_lid', 0, '#', 'render_choice', 0, '#',
  182. 'flow_label', 0, '#', 'response_label'),
  183. array(), false);
  184. $choices = array();
  185. $this->process_choices($bbchoices, $choices);
  186. $block->choices = $choices;
  187. }
  188. break;
  189. case 'RIGHT_MATCH_BLOCK':
  190. $matchinganswerset = $this->getpath($pblock,
  191. array('#', 'flow'),
  192. false, false);
  193. $answerset = array();
  194. foreach ($matchinganswerset as $answer) {
  195. $bbanswer = new stdClass;
  196. $bbanswer->text = $this->getpath($answer,
  197. array('#', 'flow', 0, '#', 'material', 0, '#', 'mat_extension',
  198. 0, '#', 'mat_formattedtext', 0, '#'),
  199. false, false);
  200. $answerset[] = $bbanswer;
  201. }
  202. $block->matchinganswerset = $answerset;
  203. break;
  204. default:
  205. $this->error(get_string('unhandledpresblock', 'qformat_blackboard_six'));
  206. break;
  207. }
  208. $rawquestion->{$block->type} = $block;
  209. }
  210. // Determine response processing.
  211. // There is a section called 'outcomes' that I don't know what to do with.
  212. $resprocessing = $this->getpath($quest,
  213. array('#', 'resprocessing'),
  214. array(), false);
  215. $respconditions = $this->getpath($resprocessing[0],
  216. array('#', 'respcondition'),
  217. array(), false);
  218. $responses = array();
  219. if ($rawquestion->qtype == 'Matching') {
  220. $this->process_matching_responses($respconditions, $responses);
  221. } else {
  222. $this->process_responses($respconditions, $responses);
  223. }
  224. $rawquestion->responses = $responses;
  225. $feedbackset = $this->getpath($quest,
  226. array('#', 'itemfeedback'),
  227. array(), false);
  228. $feedbacks = array();
  229. $this->process_feedback($feedbackset, $feedbacks);
  230. $rawquestion->feedback = $feedbacks;
  231. return $rawquestion;
  232. }
  233. /**
  234. * Helper function to process an XML block into an object.
  235. * Can call himself recursively if necessary to parse this branch of the XML tree.
  236. * @param array $curblock XML block to parse
  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 $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 $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 $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 $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 $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->responsefieldlines = 15;
  726. $question->attachments = 0;
  727. $question->responsetemplate = $this->text_field('');
  728. $questions[]=$question;
  729. }
  730. /**
  731. * Process Matching Questions
  732. * Parse a matching rawquestion and add the result
  733. * to the array of questions already parsed.
  734. * @param object $quest rawquestion
  735. * @param $questions array of Moodle questions already done.
  736. */
  737. public function process_matching($quest, &$questions) {
  738. // Blackboard matching questions can't be imported in core Moodle without a loss in data,
  739. // as core match question don't allow HTML in subanswers. The contributed ddmatch
  740. // question type support HTML in subanswers.
  741. // The ddmatch question type is not part of core, so we need to check if it is defined.
  742. $ddmatchisinstalled = question_bank::is_qtype_installed('ddmatch');
  743. $question = $this->process_common($quest);
  744. $question = $this->add_blank_combined_feedback($question);
  745. $question->valid = true;
  746. if ($ddmatchisinstalled) {
  747. $question->qtype = 'ddmatch';
  748. } else {
  749. $question->qtype = 'match';
  750. }
  751. // Construction of the array holding mappings between subanswers and subquestions.
  752. foreach ($quest->RESPONSE_BLOCK->subquestions as $qid => $subq) {
  753. foreach ($quest->responses as $rid => $resp) {
  754. if (isset($resp->ident) && $resp->ident == $subq->ident) {
  755. $correct = $resp->correct;
  756. }
  757. }
  758. foreach ($subq->choices as $cid => $choice) {
  759. if ($choice == $correct) {
  760. $mappings[$subq->ident] = $cid;
  761. }
  762. }
  763. }
  764. foreach ($subq->choices as $choiceid => $choice) {
  765. $subanswertext = $quest->RIGHT_MATCH_BLOCK->matchinganswerset[$choiceid]->text;
  766. if ($ddmatchisinstalled) {
  767. $subanswer = $this->cleaned_text_field($subanswertext);
  768. } else {
  769. $subanswertext = html_to_text($this->cleaninput($subanswertext), 0);
  770. $subanswer = $subanswertext;
  771. }
  772. if ($subanswertext != '') { // Only import non empty subanswers.
  773. $subquestion = '';
  774. $fiber = array_keys ($mappings, $choiceid);
  775. foreach ($fiber as $correctanswerid) {
  776. // We have found a correspondance for this subanswer so we need to take the associated subquestion.
  777. foreach ($quest->RESPONSE_BLOCK->subquestions as $qid => $subq) {
  778. $currentsubqid = $subq->ident;
  779. if (strcmp ($currentsubqid, $correctanswerid) == 0) {
  780. $subquestion = $subq->text;
  781. break;
  782. }
  783. }
  784. $question->subquestions[] = $this->cleaned_text_field($subquestion);
  785. $question->subanswers[] = $subanswer;
  786. }
  787. if ($subquestion == '') { // Then in this case, $choice is a distractor.
  788. $question->subquestions[] = $this->text_field('');
  789. $question->subanswers[] = $subanswer;
  790. }
  791. }
  792. }
  793. // Verify that this matching question has enough subquestions and subanswers.
  794. $subquestioncount = 0;
  795. $subanswercount = 0;
  796. $subanswers = $question->subanswers;
  797. foreach ($question->subquestions as $key => $subquestion) {
  798. $subquestion = $subquestion['text'];
  799. $subanswer = $subanswers[$key];
  800. if ($subquestion != '') {
  801. $subquestioncount++;
  802. }
  803. $subanswercount++;
  804. }
  805. if ($subquestioncount < 2 || $subanswercount < 3) {
  806. $this->error(get_string('notenoughtsubans', 'qformat_blackboard_six', $question->questiontext));
  807. } else {
  808. $questions[] = $question;
  809. }
  810. }
  811. /**
  812. * Add a category question entry based on the assessment title
  813. * @param array $xml the xml tree
  814. * @param array $questions the questions already parsed
  815. */
  816. public function process_category($xml, &$questions) {
  817. $title = $this->getpath($xml, array('questestinterop', '#', 'assessment', 0, '@', 'title'), '', true);
  818. $dummyquestion = new stdClass();
  819. $dummyquestion->qtype = 'category';
  820. $dummyquestion->category = $this->cleaninput($this->clean_question_name($title));
  821. $questions[] = $dummyquestion;
  822. }
  823. /**
  824. * Strip the applet tag used by Blackboard to render mathml formulas,
  825. * keeping the mathml tag.
  826. * @param string $string
  827. * @return string
  828. */
  829. public function strip_applet_tags_get_mathml($string) {
  830. if (stristr($string, '</APPLET>') === false) {
  831. return $string;
  832. } else {
  833. // Strip all applet tags keeping stuff before/after and inbetween (if mathml) them.
  834. while (stristr($string, '</APPLET>') !== false) {
  835. preg_match("/(.*)\<applet.*value=\"(\<math\>.*\<\/math\>)\".*\<\/applet\>(.*)/i", $string, $mathmls);
  836. $string = $mathmls[1].$mathmls[2].$mathmls[3];
  837. }
  838. return $string;
  839. }
  840. }
  841. }