PageRenderTime 51ms CodeModel.GetById 22ms RepoModel.GetById 1ms app.codeStats 0ms

/eqiat/classes/itemtypes/QTIQuestionMatrix.class.php

https://github.com/tremby/questionbank
PHP | 379 lines | 352 code | 12 blank | 15 comment | 4 complexity | 042e229f9eb8642519207ca11378ad75 MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception
  1. <?php
  2. /*
  3. * Eqiat
  4. * Easy QTI Item Authoring Tool
  5. */
  6. /*------------------------------------------------------------------------------
  7. (c) 2010 JISC-funded EASiHE project, University of Southampton
  8. Licensed under the Creative Commons 'Attribution non-commercial share alike'
  9. licence -- see the LICENCE file for more details
  10. ------------------------------------------------------------------------------*/
  11. class QTIQuestionMatrix extends QTIAssessmentItem {
  12. public function itemTypePrint() {
  13. return "question matrix";
  14. }
  15. public function itemTypeDescription() {
  16. return "A stimulus followed by a number of question prompts. The candidate selects true or false for each.";
  17. }
  18. protected function headerJS() {
  19. ob_start();
  20. ?>
  21. addquestion = function() {
  22. // clone the last question on the list and increment its id
  23. var newquestion = $("#questions tr.question:last").clone();
  24. var oldid = parseInt($("textarea", newquestion).attr("id").split("_")[1]);
  25. var newid = oldid + 1;
  26. // give it the new id number and wipe its text
  27. newquestion.attr("id", "question_" + newid);
  28. $("textarea", newquestion).attr("id", "question_" + newid + "_prompt").attr("name", "question_" + newid + "_prompt").val("").removeClass("error warning");
  29. // clear all its checkboxes and update their question numbers
  30. $("td.answer input", newquestion).removeAttr("checked");
  31. $("td.answer label.true", newquestion).attr("id", "question_" + newid + "_true");
  32. $("td.answer label.false", newquestion).attr("id", "question_" + newid + "_false");
  33. $("td.answer label input", newquestion).attr("name", "question_" + newid + "_answer");
  34. // stripe it
  35. newquestion.removeClass("row" + (oldid % 2)).addClass("row" + (newid % 2));
  36. // reinstate the remove action
  37. $("input.removequestion", newquestion).click(removequestion);
  38. // add it to the list
  39. $("#questions").append(newquestion);
  40. };
  41. removequestion = function() {
  42. if ($("#questions tr.question").size() < 2) {
  43. alert("Can't remove the last question");
  44. return;
  45. }
  46. $(this).parents("tr:first").remove();
  47. // renumber the remaining questions
  48. var i = 0;
  49. $("#questions tr.question").each(function() {
  50. $(this).attr("id", "question_" + i);
  51. $("textarea", this).attr("id", "question_" + i + "_prompt").attr("name", "question_" + i + "_prompt");
  52. $("td.answer label.true", this).attr("id", "question_" + i + "_true");
  53. $("td.answer label.false", this).attr("id", "question_" + i + "_false");
  54. $("td.answer label input", this).attr("name", "question_" + i + "_answer");
  55. $(this).removeClass("row" + ((i + 1) % 2)).addClass("row" + (i % 2));
  56. i++;
  57. });
  58. };
  59. edititemsubmitcheck_itemspecificerrors = function() {
  60. // true or false must be chosen for each question
  61. var ok = true;
  62. $("#questions tr.question td.answer").each(function(n) {
  63. if ($("input:checked", this).size() == 0) {
  64. $.scrollTo($(this).addClass("error"), scrollduration, scrolloptions);
  65. alert("No correct response has been chosen for question " + (n + 1));
  66. ok = false;
  67. return false;
  68. }
  69. });
  70. if (!ok) return false;
  71. return true;
  72. };
  73. edititemsubmitcheck_itemspecificwarnings = function() {
  74. // confirm the user wanted any empty boxes
  75. var ok = true;
  76. $("textarea.prompt").each(function(n) {
  77. if ($(this).val().length == 0) {
  78. $.scrollTo($(this).addClass("warning"), scrollduration, scrolloptions);
  79. ok = confirm("The prompt for question " + (n + 1) + " is empty -- click OK to continue regardless or cancel to edit it");
  80. if (ok)
  81. $(this).removeClass("error warning");
  82. else
  83. return false; //this is "break" in the Jquery each() pseudoloop
  84. }
  85. });
  86. if (!ok) return false;
  87. // warn about any identical questions
  88. for (var i = 0; i < $("textarea.prompt").size(); i++) {
  89. for (var j = i + 1; j < $("textarea.prompt").size(); j++) {
  90. if ($("#question_" + i + "_prompt").val() == $("#question_" + j + "_prompt").val()) {
  91. $.scrollTo($("#question_" + i + "_prompt, #question_" + j + "_prompt").addClass("warning"), scrollduration, scrolloptions);
  92. ok = confirm("The prompts for questions " + (i + 1) + " and " + (j + 1) + " are the same -- click OK to continue regardless or cancel to edit them");
  93. if (ok)
  94. $("#question_" + i + "_prompt, #question_" + j + "_prompt").removeClass("error warning");
  95. else
  96. break;
  97. }
  98. }
  99. if (!ok) break;
  100. }
  101. if (!ok) return false;
  102. // confirm the user wanted only one question
  103. if ($("textarea.prompt").size() == 1 && !confirm("There is only one question -- click OK to continue regardless or cancel to add more"))
  104. return false;
  105. return true;
  106. };
  107. $(document).ready(function() {
  108. $("#addquestion").click(addquestion);
  109. $(".removequestion").click(removequestion);
  110. });
  111. <?php
  112. return ob_get_clean();
  113. }
  114. protected function formHTML() {
  115. ob_start();
  116. ?>
  117. <dt>Questions</dt>
  118. <dd>
  119. <table id="questions">
  120. <tr>
  121. <th>Question prompt</th>
  122. <th>Correct response</th>
  123. <th>Actions</th>
  124. </tr>
  125. <?php if (!isset($this->data["question_0_prompt"])) {
  126. // starting from scratch -- initialize first questions
  127. $this->data["question_0_prompt"] = "";
  128. $this->data["question_1_prompt"] = "";
  129. }
  130. for ($i = 0; array_key_exists("question_{$i}_prompt", $this->data); $i++) { $odd = $i % 2; ?>
  131. <tr class="question row<?php echo $odd; ?>" id="question_<?php echo $i; ?>">
  132. <td><textarea class="prompt" rows="2" cols="48" name="question_<?php echo $i; ?>_prompt" id="question_<?php echo $i; ?>_prompt"><?php if (isset($this->data["question_{$i}_prompt"])) echo htmlspecialchars($this->data["question_{$i}_prompt"]); ?></textarea></td>
  133. <td class="answer">
  134. <label id="question_<?php echo $i; ?>_true" class="true">
  135. <input type="radio" name="question_<?php echo $i; ?>_answer" value="true"<?php if (isset($this->data["question_{$i}_answer"]) && $this->data["question_{$i}_answer"] == "true") { ?> checked="checked"<?php } ?>>
  136. true
  137. </label>
  138. <label id="question_<?php echo $i; ?>_false" class="false">
  139. <input type="radio" name="question_<?php echo $i; ?>_answer" value="false"<?php if (isset($this->data["question_{$i}_answer"]) && $this->data["question_{$i}_answer"] != "true") { ?> checked="checked"<?php } ?>>
  140. false
  141. </label>
  142. </td>
  143. <td><input type="button" class="removequestion" value="Remove"></td>
  144. </tr>
  145. <?php } ?>
  146. </table>
  147. <input type="button" id="addquestion" value="Add question">
  148. </dd>
  149. <?php
  150. return ob_get_clean();
  151. }
  152. public function buildQTI($data = null) {
  153. if (!is_null($data))
  154. $this->data = $data;
  155. if (empty($this->data))
  156. return false;
  157. // container element and other metadata
  158. $ai = $this->initialXML();
  159. // response declarations
  160. for ($q = 0; array_key_exists("question_{$q}_prompt", $this->data); $q++) {
  161. $rd = $ai->addChild("responseDeclaration");
  162. $rd->addAttribute("identifier", "RESPONSE_question_$q");
  163. $rd->addAttribute("cardinality", "single");
  164. $rd->addAttribute("baseType", "identifier");
  165. if (isset($this->data["question_{$q}_answer"]))
  166. $rd->addChild("correctResponse")->addChild("value", "question_{$q}_" . $this->data["question_{$q}_answer"]);
  167. }
  168. // outcome declaration
  169. $od = $ai->addChild("outcomeDeclaration");
  170. $od->addAttribute("identifier", "SCORE");
  171. $od->addAttribute("cardinality", "single");
  172. $od->addAttribute("baseType", "integer");
  173. $od->addChild("defaultValue");
  174. $od->defaultValue->addChild("value", "0");
  175. // item body
  176. $ib = $ai->addChild("itemBody");
  177. // get stimulus and add to the XML tree
  178. if (isset($this->data["stimulus"]) && !empty($this->data["stimulus"])) {
  179. $this->data["stimulus"] = wrapindiv($this->data["stimulus"]);
  180. // parse it as XML
  181. $stimulus = stringtoxml($this->data["stimulus"], "stimulus");
  182. if (is_array($stimulus)) {
  183. // errors
  184. $this->errors[] = "Stimulus is not valid XML. It must not only be valid XML but valid QTI, which accepts a subset of XHTML. Details on specific issues follow:";
  185. $this->errors = array_merge($this->errors, $stimulus);
  186. } else
  187. simplexml_append($ib, $stimulus);
  188. }
  189. // div with class eqiat-qm
  190. $d = $ib->addChild("div");
  191. $d->addAttribute("class", "eqiat-qm");
  192. // questions
  193. for ($q = 0; array_key_exists("question_{$q}_prompt", $this->data); $q++) {
  194. $ci = $d->addChild("choiceInteraction");
  195. $ci->addAttribute("maxChoices", "1");
  196. $ci->addAttribute("shuffle", "false");
  197. $ci->addAttribute("responseIdentifier", "RESPONSE_question_$q");
  198. $ci->addChild("prompt", $this->data["question_{$q}_prompt"]);
  199. foreach (array("true", "false") as $o) {
  200. $sc = $ci->addChild("simpleChoice", $o);
  201. $sc->addAttribute("identifier", "question_{$q}_$o");
  202. }
  203. }
  204. // response processing
  205. $rp = $ai->addChild("responseProcessing");
  206. // set score = 0
  207. $sov = $rp->addChild("setOutcomeValue");
  208. $sov->addAttribute("identifier", "SCORE");
  209. $sov->addChild("baseValue", "0")->addAttribute("baseType", "integer");
  210. for ($q = 0; array_key_exists("question_{$q}_prompt", $this->data); $q++) {
  211. $rc = $rp->addChild("responseCondition");
  212. // if
  213. $ri = $rc->addChild("responseIf");
  214. // criteria for a correct answer
  215. $m = $ri->addChild("match");
  216. $m->addChild("variable")->addAttribute("identifier", "RESPONSE_question_$q");
  217. $m->addChild("correct")->addAttribute("identifier", "RESPONSE_question_$q");
  218. // increment score
  219. $sov = $ri->addChild("setOutcomeValue");
  220. $sov->addAttribute("identifier", "SCORE");
  221. $s = $sov->addChild("sum");
  222. $s->addChild("variable")->addAttribute("identifier", "SCORE");
  223. $s->addChild("baseValue", "1")->addAttribute("baseType", "integer");
  224. }
  225. if (!empty($this->errors))
  226. return false;
  227. // validate the QTI
  228. validateQTI($ai, $this->errors, $this->warnings, $this->messages);
  229. if (!empty($this->errors))
  230. return false;
  231. $this->qti = $ai;
  232. return $this->qti;
  233. }
  234. public function fromXML(SimpleXMLElement $xml) {
  235. $data = array(
  236. "itemtype" => $this->itemType(),
  237. "title" => (string) $xml["title"],
  238. "stimulus" => qti_get_stimulus($xml->itemBody),
  239. );
  240. // check for a div with the item class name
  241. $itembodycontainer = null;
  242. foreach ($xml->itemBody->div as $div) {
  243. if (!isset($div["class"]) || (string) $div["class"] != "eqiat-qm")
  244. continue;
  245. // get elements from here
  246. $itembodycontainer = $div;
  247. break;
  248. }
  249. // if there was none, get elements from itemBody
  250. if (is_null($itembodycontainer))
  251. $itembodycontainer = $xml->itemBody;
  252. // count the choiceInteractions
  253. $questioncount = count($itembodycontainer->choiceInteraction);
  254. // no good if there are no questions
  255. if ($questioncount == 0)
  256. return 0;
  257. // ensure there are the same number of responseDeclarations
  258. if (count($xml->responseDeclaration) != $questioncount)
  259. return 0;
  260. // ensure there are the same number of responseConditions
  261. if (count($xml->responseProcessing->responseCondition) != $questioncount)
  262. return 0;
  263. // ensure some stuff for each question
  264. $q = 0;
  265. foreach ($itembodycontainer->choiceInteraction as $ci) {
  266. // candidate can only choose one answer
  267. if ((string) $ci["maxChoices"] != "1")
  268. return 0;
  269. // there are two possible answers
  270. if (count($ci->simpleChoice) != 2)
  271. return 0;
  272. // answers are true and false
  273. $answers = array(null, null);
  274. foreach ($ci->simpleChoice as $sc) {
  275. if (strtolower((string) $sc) == "false")
  276. $answers[0] = (string) $sc["identifier"];
  277. else if (strtolower((string) $sc) == "true")
  278. $answers[1] = (string) $sc["identifier"];
  279. else
  280. return 0;
  281. }
  282. // check some responseDeclaration things
  283. $declarationsfound = 0;
  284. foreach ($xml->responseDeclaration as $rd) {
  285. if ((string) $rd["identifier"] != (string) $ci["responseIdentifier"])
  286. continue;
  287. $declarationsfound++;
  288. // has a correct response
  289. if (!isset($rd->correctResponse))
  290. return 0;
  291. // has one correct response
  292. if (count($rd->correctResponse->value) != 1)
  293. return 0;
  294. // the correct response is one of the options
  295. $answer = array_search((string) $rd->correctResponse->value, $answers);
  296. if ($answer === false)
  297. return 0;
  298. // add answer to data
  299. $data["question_{$q}_answer"] = $answer ? true : false;
  300. }
  301. // there was a good responseDeclaration for this question
  302. if ($declarationsfound != 1)
  303. return 0;
  304. // add prompt to data
  305. $data["question_{$q}_prompt"] = (string) $ci->prompt;
  306. $q++;
  307. }
  308. // happy with that -- set data property and identifier
  309. $this->data = $data;
  310. $this->setQTIID((string) $xml["identifier"]);
  311. // rather strange question matrix if it's only one question
  312. if ($questioncount == 1)
  313. return 127;
  314. return 255;
  315. }
  316. }
  317. ?>