qti 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +28 -0
  3. data/Rakefile +1 -0
  4. data/lib/qti.rb +62 -0
  5. data/lib/qti/models/base.rb +80 -0
  6. data/lib/qti/models/manifest.rb +20 -0
  7. data/lib/qti/v1/models/assessment.rb +17 -0
  8. data/lib/qti/v1/models/assessment_item.rb +59 -0
  9. data/lib/qti/v1/models/base.rb +13 -0
  10. data/lib/qti/v1/models/choices/logical_identifier_choice.rb +26 -0
  11. data/lib/qti/v1/models/interactions/logical_identifier_interaction.rb +32 -0
  12. data/lib/qti/v2/models/assessment_item.rb +62 -0
  13. data/lib/qti/v2/models/assessment_test.rb +32 -0
  14. data/lib/qti/v2/models/base.rb +70 -0
  15. data/lib/qti/v2/models/choices/simple_choice.rb +28 -0
  16. data/lib/qti/v2/models/interactions.rb +25 -0
  17. data/lib/qti/v2/models/interactions/choice_interaction.rb +50 -0
  18. data/spec/fixtures/items_1.2/multiple_answer.xml +105 -0
  19. data/spec/fixtures/items_1.2/multiple_choice.xml +83 -0
  20. data/spec/fixtures/items_1.2/true_false.xml +50 -0
  21. data/spec/fixtures/items_2.1/Example01-modalFeedback.xml +79 -0
  22. data/spec/fixtures/items_2.1/Example02-feedbackInline.xml +67 -0
  23. data/spec/fixtures/items_2.1/Example03-feedbackBlock-solution-random.xml +223 -0
  24. data/spec/fixtures/items_2.1/Example03-feedbackBlock-solution.xml +238 -0
  25. data/spec/fixtures/items_2.1/Example04-feedbackBlock-templateBlock.xml +582 -0
  26. data/spec/fixtures/items_2.1/Example05-feedbackBlock-adaptive.xml +397 -0
  27. data/spec/fixtures/items_2.1/adaptive.xml +286 -0
  28. data/spec/fixtures/items_2.1/adaptive_template.xml +350 -0
  29. data/spec/fixtures/items_2.1/associate.xml +33 -0
  30. data/spec/fixtures/items_2.1/audio-video.xml +46 -0
  31. data/spec/fixtures/items_2.1/choice.xml +31 -0
  32. data/spec/fixtures/items_2.1/choice_aria.xml +42 -0
  33. data/spec/fixtures/items_2.1/choice_fixed.xml +33 -0
  34. data/spec/fixtures/items_2.1/choice_multiple.xml +31 -0
  35. data/spec/fixtures/items_2.1/choice_multiple_chocolade.xml +74 -0
  36. data/spec/fixtures/items_2.1/choice_multiple_rtl.xml +33 -0
  37. data/spec/fixtures/items_2.1/choice_ruby.xml +39 -0
  38. data/spec/fixtures/items_2.1/choice_t_f.xml +30 -0
  39. data/spec/fixtures/items_2.1/choice_true_false.xml +30 -0
  40. data/spec/fixtures/items_2.1/data-attributes.xml +61 -0
  41. data/spec/fixtures/items_2.1/drawing.xml +16 -0
  42. data/spec/fixtures/items_2.1/essay.xml +14 -0
  43. data/spec/fixtures/items_2.1/extended_text.xml +24 -0
  44. data/spec/fixtures/items_2.1/extended_text_rubric.xml +36 -0
  45. data/spec/fixtures/items_2.1/feedback_adaptive.xml +142 -0
  46. data/spec/fixtures/items_2.1/figures.xml +32 -0
  47. data/spec/fixtures/items_2.1/gap_match.xml +33 -0
  48. data/spec/fixtures/items_2.1/graphic_associate.xml +36 -0
  49. data/spec/fixtures/items_2.1/graphic_gap_match.xml +54 -0
  50. data/spec/fixtures/items_2.1/graphic_gap_match_text.xml +50 -0
  51. data/spec/fixtures/items_2.1/graphic_order.xml +32 -0
  52. data/spec/fixtures/items_2.1/hint.xml +85 -0
  53. data/spec/fixtures/items_2.1/hotspot.xml +26 -0
  54. data/spec/fixtures/items_2.1/hottext.xml +33 -0
  55. data/spec/fixtures/items_2.1/images/CBG.png +0 -0
  56. data/spec/fixtures/items_2.1/images/EBG.png +0 -0
  57. data/spec/fixtures/items_2.1/images/EDI.png +0 -0
  58. data/spec/fixtures/items_2.1/images/GLA.png +0 -0
  59. data/spec/fixtures/items_2.1/images/MAN.png +0 -0
  60. data/spec/fixtures/items_2.1/images/MCH.png +0 -0
  61. data/spec/fixtures/items_2.1/images/airport.png +0 -0
  62. data/spec/fixtures/items_2.1/images/blueStar.png +0 -0
  63. data/spec/fixtures/items_2.1/images/blue_door.png +0 -0
  64. data/spec/fixtures/items_2.1/images/bus.png +0 -0
  65. data/spec/fixtures/items_2.1/images/castle.png +0 -0
  66. data/spec/fixtures/items_2.1/images/greenTriangle.png +0 -0
  67. data/spec/fixtures/items_2.1/images/green_door.png +0 -0
  68. data/spec/fixtures/items_2.1/images/house.png +0 -0
  69. data/spec/fixtures/items_2.1/images/open_car.png +0 -0
  70. data/spec/fixtures/items_2.1/images/open_goat.png +0 -0
  71. data/spec/fixtures/items_2.1/images/plane.png +0 -0
  72. data/spec/fixtures/items_2.1/images/postcard.eps +3581 -0
  73. data/spec/fixtures/items_2.1/images/postcard.png +0 -0
  74. data/spec/fixtures/items_2.1/images/rectangle.svg +1 -0
  75. data/spec/fixtures/items_2.1/images/redCircle.png +0 -0
  76. data/spec/fixtures/items_2.1/images/red_door.png +0 -0
  77. data/spec/fixtures/items_2.1/images/sign.png +0 -0
  78. data/spec/fixtures/items_2.1/images/survey.jpg +0 -0
  79. data/spec/fixtures/items_2.1/images/train.png +0 -0
  80. data/spec/fixtures/items_2.1/images/uk.png +0 -0
  81. data/spec/fixtures/items_2.1/images/ukair.png +0 -0
  82. data/spec/fixtures/items_2.1/images/ukairtags.png +0 -0
  83. data/spec/fixtures/items_2.1/imsmanifest.xml +227 -0
  84. data/spec/fixtures/items_2.1/inline_choice.xml +26 -0
  85. data/spec/fixtures/items_2.1/inline_choice_math.xml +68 -0
  86. data/spec/fixtures/items_2.1/likert.xml +21 -0
  87. data/spec/fixtures/items_2.1/match.xml +41 -0
  88. data/spec/fixtures/items_2.1/math.xml +35 -0
  89. data/spec/fixtures/items_2.1/mc_calc3.xml +164 -0
  90. data/spec/fixtures/items_2.1/mc_calc5.xml +211 -0
  91. data/spec/fixtures/items_2.1/mc_stat2.xml +219 -0
  92. data/spec/fixtures/items_2.1/media_coords.xml +33 -0
  93. data/spec/fixtures/items_2.1/multi-input.xml +271 -0
  94. data/spec/fixtures/items_2.1/nested_object.xml +26 -0
  95. data/spec/fixtures/items_2.1/order.xml +25 -0
  96. data/spec/fixtures/items_2.1/order_partial_scoring.xml +55 -0
  97. data/spec/fixtures/items_2.1/order_rtl.xml +27 -0
  98. data/spec/fixtures/items_2.1/orkney1.xml +34 -0
  99. data/spec/fixtures/items_2.1/orkney2.xml +34 -0
  100. data/spec/fixtures/items_2.1/position_object.xml +34 -0
  101. data/spec/fixtures/items_2.1/select_point.xml +23 -0
  102. data/spec/fixtures/items_2.1/shared/orkney.css +1 -0
  103. data/spec/fixtures/items_2.1/shared/orkney.html +20 -0
  104. data/spec/fixtures/items_2.1/slider.xml +35 -0
  105. data/spec/fixtures/items_2.1/svg.xml +28 -0
  106. data/spec/fixtures/items_2.1/template.xml +88 -0
  107. data/spec/fixtures/items_2.1/template_image.xml +74 -0
  108. data/spec/fixtures/items_2.1/text_entry.xml +27 -0
  109. data/spec/fixtures/items_2.1/upload.xml +18 -0
  110. data/spec/fixtures/items_2.1/upload_composite.xml +24 -0
  111. data/spec/fixtures/test_qti_1.2.zip +0 -0
  112. data/spec/fixtures/test_qti_1.2/contact info.png +0 -0
  113. data/spec/fixtures/test_qti_1.2/exam1/my_files/org0/images/image.png +0 -0
  114. data/spec/fixtures/test_qti_1.2/exam1/my_files/org1/images/image.png +0 -0
  115. data/spec/fixtures/test_qti_1.2/image.png +0 -0
  116. data/spec/fixtures/test_qti_1.2/imsmanifest.xml +37 -0
  117. data/spec/fixtures/test_qti_1.2/quiz.xml +216 -0
  118. data/spec/fixtures/test_qti_2.1.zip +0 -0
  119. data/spec/fixtures/test_qti_2.1/assessment.xml +77 -0
  120. data/spec/fixtures/test_qti_2.1/imsmanifest.xml +211 -0
  121. data/spec/fixtures/test_qti_2.1/mathassess.xsd +54 -0
  122. data/spec/fixtures/test_qti_2.1/multiple-answer/multiple-answer.xml +31 -0
  123. data/spec/fixtures/test_qti_2.1/multiple-choice/multiple-choice.xml +31 -0
  124. data/spec/fixtures/test_qti_2.1/text-entry/text-entry.xml +330 -0
  125. data/spec/fixtures/test_qti_2.1/true-false/true-false.xml +55 -0
  126. data/spec/fixtures/tests/feedbackTest/assessment.xml +252 -0
  127. data/spec/fixtures/tests/feedbackTest/id-38d47747fb30/Example02-feedbackInline.xml +51 -0
  128. data/spec/fixtures/tests/feedbackTest/id-5e216df10030/Example03-feedbackBlock-solution.xml +199 -0
  129. data/spec/fixtures/tests/feedbackTest/id-9c33d141576e/Example05-feedbackBlock-adaptive.xml +330 -0
  130. data/spec/fixtures/tests/feedbackTest/id-aaa0bbf5da1d/Example03-feedbackBlock-solution-random.xml +223 -0
  131. data/spec/fixtures/tests/feedbackTest/id-d599169403d2/Example04-feedbackBlock-templateBlock.xml +534 -0
  132. data/spec/fixtures/tests/feedbackTest/id-fa3ee5852723/Example01-modalFeedback.xml +55 -0
  133. data/spec/fixtures/tests/feedbackTest/imsmanifest.xml +239 -0
  134. data/spec/fixtures/tests/interactionmix_saxony_v3/Choicemultiple_2014410822.xml +110 -0
  135. data/spec/fixtures/tests/interactionmix_saxony_v3/Choicemultiple_871212949.xml +208 -0
  136. data/spec/fixtures/tests/interactionmix_saxony_v3/Choicesingle_853928446.xml +159 -0
  137. data/spec/fixtures/tests/interactionmix_saxony_v3/Choicetruefalse_176040516.xml +98 -0
  138. data/spec/fixtures/tests/interactionmix_saxony_v3/Hotspot_278940407.xml +109 -0
  139. data/spec/fixtures/tests/interactionmix_saxony_v3/Hottext_801974120.xml +106 -0
  140. data/spec/fixtures/tests/interactionmix_saxony_v3/InteractionMixSachsen_1901710679.xml +54 -0
  141. data/spec/fixtures/tests/interactionmix_saxony_v3/Matchmultiple_1038910213.xml +139 -0
  142. data/spec/fixtures/tests/interactionmix_saxony_v3/Matchsingle_143114773.xml +133 -0
  143. data/spec/fixtures/tests/interactionmix_saxony_v3/Order_913967682.xml +117 -0
  144. data/spec/fixtures/tests/interactionmix_saxony_v3/TextEntry_883368511.xml +114 -0
  145. data/spec/fixtures/tests/interactionmix_saxony_v3/TextEntrynumeric_2040297025.xml +154 -0
  146. data/spec/fixtures/tests/interactionmix_saxony_v3/TextEntrynumeric_770468849.xml +124 -0
  147. data/spec/fixtures/tests/interactionmix_saxony_v3/TextEntrysubset_806481421.xml +227 -0
  148. data/spec/fixtures/tests/interactionmix_saxony_v3/imsmanifest.xml +278 -0
  149. data/spec/fixtures/tests/interactionmix_saxony_v3/media/03c5e2f774db49598deb3ac7d05b53f9.jpg +0 -0
  150. data/spec/fixtures/tests/interactionmix_saxony_v3/media/0563e67fb1bb48ad882e0a5abf2a2686.jpg +0 -0
  151. data/spec/fixtures/tests/interactionmix_saxony_v3/media/2f44366716474a5d8e2a2e32346cbc05.jpg +0 -0
  152. data/spec/fixtures/tests/interactionmix_saxony_v3/media/71a299b50b5e4b1492f8ee51ce25be7d.jpg +0 -0
  153. data/spec/fixtures/tests/interactionmix_saxony_v3/media/a439b702d5034ca38f6df26a12835db3.jpg +0 -0
  154. data/spec/fixtures/tests/interactionmix_saxony_v3/media/b660fc9f268c4ef688d8606116c70160.jpg +0 -0
  155. data/spec/fixtures/tests/interactionmix_saxony_v3/media/c2f541343b9b4bfdba9fc90c7a18403e.jpg +0 -0
  156. data/spec/fixtures/tests/interactionmix_saxony_v3/media/e18b5a2eb8104d0994a182ee0176a05c.jpg +0 -0
  157. data/spec/fixtures/tests/interactionmix_saxony_v3/media/f88c5aa2c6724a388fdbadc7133eb757.jpg +0 -0
  158. data/spec/fixtures/tests/interactionmix_saxony_v3/media/ffe337505ef74334a0cecb33310f99c1.jpg +0 -0
  159. data/spec/fixtures/tests/tests/complete.xml +33 -0
  160. data/spec/fixtures/tests/tests/report.xml +70 -0
  161. data/spec/fixtures/tests/tests/rtest01-fragment.xml +12 -0
  162. data/spec/fixtures/tests/tests/rtest01-set01.xml +17 -0
  163. data/spec/fixtures/tests/tests/rtest01-set02.xml +17 -0
  164. data/spec/fixtures/tests/tests/rtest01-set03.xml +17 -0
  165. data/spec/fixtures/tests/tests/rtest01.xml +16 -0
  166. data/spec/fixtures/tests/tests/rtest02.xml +48 -0
  167. data/spec/fixtures/tests/tests/rtest03.xml +38 -0
  168. data/spec/fixtures/tests/tests/rtest04.xml +50 -0
  169. data/spec/fixtures/tests/tests/rtest06.xml +34 -0
  170. data/spec/fixtures/tests/tests/rtest08.xml +38 -0
  171. data/spec/fixtures/tests/tests/rtest09.xml +33 -0
  172. data/spec/fixtures/tests/tests/rtest10.xml +48 -0
  173. data/spec/fixtures/tests/tests/rtest12.xml +38 -0
  174. data/spec/fixtures/tests/tests/rtest13.xml +62 -0
  175. data/spec/fixtures/tests/tests/rtest22.xml +41 -0
  176. data/spec/fixtures/tests/tests/rtest24.xml +37 -0
  177. data/spec/fixtures/tests/tests/rtest25.xml +62 -0
  178. data/spec/fixtures/tests/tests/rtest26.xml +38 -0
  179. data/spec/fixtures/tests/tests/rtest27.xml +46 -0
  180. data/spec/qti/models/base_spec.rb +71 -0
  181. data/spec/qti/models/manifest_spec.rb +19 -0
  182. data/spec/qti/v1/models/assessment_item_spec.rb +49 -0
  183. data/spec/qti/v1/models/assessment_spec.rb +18 -0
  184. data/spec/qti/v1/models/choices/logical_identifier_choice_spec.rb +23 -0
  185. data/spec/qti/v1/models/interactions/logical_identifier_interaction_spec.rb +20 -0
  186. data/spec/qti/v2/models/assessment_item_spec.rb +67 -0
  187. data/spec/qti/v2/models/assessment_test_spec.rb +60 -0
  188. data/spec/qti/v2/models/choices/simple_choice_spec.rb +22 -0
  189. data/spec/qti/v2/models/interactions/choice_interaction_spec.rb +27 -0
  190. data/spec/qti_spec.rb +38 -0
  191. data/spec/spec_helper.rb +10 -0
  192. metadata +506 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 393747e20e5dfd4ea82c84d94a453ff255dca004
4
+ data.tar.gz: 4895db6433e8b5b37775888cffc6b884103637b2
5
+ SHA512:
6
+ metadata.gz: 025acd7b5b163a07ff92daf38373037fe62ae82a6a46aa0101f95d0d924186dcecd467d5a4c585cb4e35b20d94e7e716b00c1070719b60871f3847a23f017506
7
+ data.tar.gz: 5bc328455e6b63cb3e543d0199e135aee3696b2d66be5994732ba7ae36dccc6211602870887c6c1d7b0f633caa2275238b718d88e56f82a7e8bf2b880faa325a
@@ -0,0 +1,28 @@
1
+ # Qti Gem
2
+
3
+ The Qti gem supports QTI 1.2 and 2.1. It currently supports the following interaction types:
4
+
5
+ - True/False
6
+ - Multiple Choice
7
+ - Multiple Answer
8
+
9
+ ### Installation
10
+
11
+ ```sh
12
+ $ cd your_project
13
+ $ bundle install qti
14
+ ```
15
+
16
+ ### Usage
17
+
18
+ `require 'qti'`
19
+ `@gem = Qti::Importer.new(path_of_quiz)`
20
+
21
+ Now you can use the gem to access the manifest, the assessments, and the assessment items and use it in your own code!
22
+
23
+ ### Development
24
+
25
+ Want to contribute? Submit a pull request!
26
+
27
+ ### To Do
28
+ - More interaction models!
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,62 @@
1
+ require 'find'
2
+
3
+ module Qti
4
+ class Importer
5
+ def initialize(path)
6
+ Find.find(path) do |subdir|
7
+ @path = subdir if subdir =~ /imsmanifest.xml/
8
+ end
9
+ end
10
+
11
+ def import_manifest
12
+ @manifest = Qti::Models::Manifest.from_path!(@path)
13
+ raise 'Unsupported QTI version' if @manifest.assessment_test_href.nil?
14
+ @manifest.assessment_test_href
15
+ end
16
+
17
+ def test_object
18
+ version_agnostic_test_object(import_manifest)
19
+ end
20
+
21
+ def version_agnostic_test_object(assessment_test_file)
22
+ if @manifest.qti_1_href
23
+ Qti::V1::Models::Assessment.from_path!(assessment_test_file)
24
+ else
25
+ Qti::V2::Models::AssessmentTest.from_path!(assessment_test_file)
26
+ end
27
+ end
28
+
29
+ def assessment_item_refs
30
+ @assessment_item_refs ||= begin
31
+ if @manifest.qti_1_href
32
+ test_object.assessment_items
33
+ else
34
+ test_object.assessment_item_reference_hrefs
35
+ end
36
+ end
37
+ end
38
+
39
+ def create_assessment_item(assessment_item_ref)
40
+ if @manifest.qti_1_href
41
+ Qti::V1::Models::AssessmentItem.new(assessment_item_ref)
42
+ else
43
+ Qti::V2::Models::AssessmentItem.from_path!(assessment_item_ref)
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ require 'qti/models/manifest'
50
+ require 'qti/models/base'
51
+
52
+ require 'qti/v1/models/base'
53
+ require 'qti/v1/models/assessment'
54
+ require 'qti/v1/models/assessment_item'
55
+ require 'qti/v1/models/choices/logical_identifier_choice'
56
+ require 'qti/v1/models/interactions/logical_identifier_interaction'
57
+
58
+ require 'qti/v2/models/base'
59
+ require 'qti/v2/models/choices/simple_choice'
60
+ require 'qti/v2/models/interactions/choice_interaction'
61
+ require 'qti/v2/models/assessment_item'
62
+ require 'qti/v2/models/assessment_test'
@@ -0,0 +1,80 @@
1
+ require 'nokogiri'
2
+ require 'sanitize'
3
+
4
+ module Qti
5
+ class ParseError < StandardError; end
6
+ class SpecificationViolation < StandardError; end
7
+ class UnsupportedSchema < StandardError; end
8
+
9
+ module Models
10
+ class Base
11
+ ELEMENTS_REMAP = {
12
+ 'prompt' => 'div',
13
+ 'simpleBlock' => 'div',
14
+ 'simpleInline' => 'span',
15
+ 'atomicBlock' => 'div',
16
+ 'atomicInline' => 'span'
17
+ }.freeze
18
+
19
+ def sanitize_content!(html)
20
+ Sanitize.fragment(html, sanitize_config)
21
+ end
22
+
23
+ def remap_unknown_tags_transformer
24
+ lambda do |env|
25
+ node_name = env[:node_name]
26
+ node = env[:node]
27
+
28
+ return if env[:is_whitelisted] || !node.element?
29
+ return unless ELEMENTS_REMAP.keys.include? node_name
30
+
31
+ new_name = ELEMENTS_REMAP[node_name]
32
+ env[:node].name = new_name
33
+ env[:node_name] = new_name
34
+ end
35
+ end
36
+
37
+ def sanitize_config
38
+ Sanitize::Config::RELAXED.merge transformers: remap_unknown_tags_transformer
39
+ end
40
+
41
+ def self.from_path!(path)
42
+ new(path: path)
43
+ end
44
+
45
+ def initialize(path: nil)
46
+ @path = path
47
+ @doc = parse_xml(File.read(path))
48
+ raise ArgumentError unless @doc
49
+ end
50
+
51
+ def xpath_with_single_check(xpath)
52
+ node_list = @doc.xpath(xpath)
53
+ raise Qti::ParseError, 'Too many matches' if node_list.count > 1
54
+ node_list.first
55
+ end
56
+
57
+ def css_with_single_check(css)
58
+ node_list = @doc.css(css)
59
+ raise Qti::ParseError, 'Too many matches' if node_list.count > 1
60
+ node_list.first
61
+ end
62
+
63
+ def parse_xml(xml_string)
64
+ Nokogiri.XML(xml_string, &:noblanks)
65
+ end
66
+
67
+ def remap_href_path(href, source_path)
68
+ return href unless href
69
+ # Attempts to map to a file path relative href if href doesn't exist
70
+ # Returns original href if that file doesn't exist
71
+ if File.exist?(href)
72
+ href
73
+ else
74
+ new_path = File.join(File.dirname(source_path), href)
75
+ File.exist?(new_path) ? new_path : href
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,20 @@
1
+ require 'qti/v1/models/base'
2
+
3
+ module Qti
4
+ module Models
5
+ class Manifest < Qti::Models::Base
6
+ def assessment_test_href
7
+ href = qti_1_href.nil? ? qti_2_href : qti_1_href
8
+ remap_href_path(href, @path)
9
+ end
10
+
11
+ def qti_1_href
12
+ xpath_with_single_check("//xmlns:resources/xmlns:resource[@type='imsqti_xmlv1p1']/@href")&.content
13
+ end
14
+
15
+ def qti_2_href
16
+ xpath_with_single_check("//xmlns:resources/xmlns:resource[@type='imsqti_test_xmlv2p2']/@href")&.content
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,17 @@
1
+ require 'qti/v1/models/base'
2
+
3
+ module Qti
4
+ module V1
5
+ module Models
6
+ class Assessment < Qti::V1::Models::Base
7
+ def title
8
+ @title ||= xpath_with_single_check('//assessment/@title').content
9
+ end
10
+
11
+ def assessment_items
12
+ @doc.xpath('//item')
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,59 @@
1
+ require 'qti/v1/models/interactions/logical_identifier_interaction'
2
+
3
+ module Qti
4
+ module V1
5
+ module Models
6
+ class AssessmentItem < Qti::V1::Models::Base
7
+ ScoringData = Struct.new(:values, :rcardinality)
8
+
9
+ def initialize(item)
10
+ @doc = item
11
+ end
12
+
13
+ def item_body
14
+ @item_body ||= begin
15
+ node = @doc.dup
16
+ prompt = node.at_xpath('./presentation/material/mattext').content
17
+ sanitize_content!(prompt)
18
+ end
19
+ end
20
+
21
+ def identifier
22
+ @identifier ||= @doc.attribute('ident').value
23
+ end
24
+
25
+ def title
26
+ @title ||= @doc.attribute('title').value
27
+ end
28
+
29
+ def points_possible
30
+ @points_possible ||= begin
31
+ @doc.at_xpath('.//decvar/@maxvalue')&.value || @doc.at_xpath('.//decvar/@defaultval')&.value
32
+ end
33
+ end
34
+
35
+ def rcardinality
36
+ @rcardinality ||= @doc.at_xpath('.//response_lid/@rcardinality').value
37
+ end
38
+
39
+ def interaction_model
40
+ @interaction_model ||= V1::Models::Interactions::LogicalIdentifierInteraction.new(@doc)
41
+ end
42
+
43
+ def scoring_data_structs
44
+ @scoring_data_structs ||= begin
45
+ rcardinality = @doc.at_xpath('//response_lid/@rcardinality').value
46
+
47
+ choice_nodes = @doc.xpath('.//respcondition')
48
+ value_nodes = []
49
+ # We need to find an action of Set or Add greater than 0 bc there is no correct answer tag
50
+ choice_nodes.each do |choice_node|
51
+ value_nodes << choice_node if choice_node.at_xpath('.//setvar').content.to_f.positive?
52
+ end
53
+ value_nodes.map { |value_node| ScoringData.new(value_node.at_xpath('//varequal').content, rcardinality) }
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,13 @@
1
+ require 'qti/models/base'
2
+
3
+ module Qti
4
+ module V1
5
+ module Models
6
+ class Base < Qti::Models::Base
7
+ def qti_version
8
+ 1
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,26 @@
1
+ require 'qti/v1/models/base'
2
+
3
+ module Qti
4
+ module V1
5
+ module Models
6
+ module Choices
7
+ class LogicalIdentifierChoice < Qti::V1::Models::Base
8
+ def initialize(node)
9
+ @node = node
10
+ end
11
+
12
+ def identifier
13
+ @identifier ||= @node.attributes['ident'].value
14
+ end
15
+
16
+ def item_body
17
+ @item_body ||= begin
18
+ node = @node.dup
19
+ node.content.strip.gsub(/\s+/, ' ')
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,32 @@
1
+ require 'qti/v1/models/base'
2
+ require 'qti/v1/models/choices/logical_identifier_choice'
3
+
4
+ module Qti
5
+ module V1
6
+ module Models
7
+ module Interactions
8
+ class LogicalIdentifierInteraction < Qti::V1::Models::Base
9
+ def initialize(node)
10
+ @node = node
11
+ end
12
+
13
+ def shuffled?
14
+ @node.at_xpath('.//render_choice/@shuffle')&.value == 'Yes'
15
+ end
16
+
17
+ def answers
18
+ @answers ||= answer_nodes.map do |node|
19
+ V1::Models::Choices::LogicalIdentifierChoice.new(node)
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def answer_nodes
26
+ @node.xpath('.//response_label')
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,62 @@
1
+ require 'qti/v2/models/base'
2
+ require 'qti/v2/models/interactions'
3
+
4
+ module Qti
5
+ module V2
6
+ module Models
7
+ class AssessmentItem < Qti::V2::Models::Base
8
+ ScoringData = Struct.new(:values, :type)
9
+
10
+ def item_body
11
+ @item_body ||= begin
12
+ node = item_body_node.dup
13
+ # ensure a prompt is carried into the html
14
+ prompt = node.at_xpath('//xmlns:prompt')
15
+
16
+ # Filter undesired interaction nodes out of the list (need to make this a deep traversal)
17
+ node.children.filter(INTERACTION_ELEMENTS_CSS).map(&:unlink)
18
+
19
+ node.add_child(prompt) if prompt&.parent && prompt.parent != node
20
+ sanitize_content!(node.to_html)
21
+ end
22
+ end
23
+
24
+ # Not used yet
25
+ def identifier
26
+ @identifier ||= xpath_with_single_check('//xmlns:assessmentItem/@identifier').content
27
+ end
28
+
29
+ def title
30
+ @title ||= xpath_with_single_check('//xmlns:assessmentItem/@title').content
31
+ end
32
+
33
+ def points_possible
34
+ @points_possible ||= begin
35
+ xpath_with_single_check("//xmlns:outcomeDeclaration[@identifier='SCORE']/@normalMaximum")&.content ||
36
+ xpath_with_single_check("//xmlns:outcomeDeclaration[@identifier='MAXSCORE']//xmlns:value")&.content
37
+ end
38
+ end
39
+
40
+ def interaction_model
41
+ @interaction_model ||= begin
42
+ V2::Models::Interactions.interaction_model(item_body_node)
43
+ end
44
+ end
45
+
46
+ def scoring_data_structs
47
+ @scoring_data_structs ||= begin
48
+ type = xpath_with_single_check('//xmlns:responseDeclaration/@baseType').content
49
+ value_nodes = @doc.xpath('//xmlns:responseDeclaration/xmlns:correctResponse/xmlns:value')
50
+ value_nodes.map { |value_node| ScoringData.new(value_node.content, type) }
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def item_body_node
57
+ @item_body_node ||= xpath_with_single_check('//xmlns:itemBody')
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,32 @@
1
+ require 'qti/v2/models/base'
2
+
3
+ module Qti
4
+ module V2
5
+ module Models
6
+ class AssessmentTest < Qti::V2::Models::Base
7
+ def title
8
+ @title ||= xpath_with_single_check('//xmlns:assessmentTest/@title').content
9
+ end
10
+
11
+ def assessment_item_reference_hrefs
12
+ # Return the xml files we should be parsing
13
+ @assessment_item_reference_hrefs ||= begin
14
+ @doc.xpath('//xmlns:assessmentItemRef/@href').map(&:content).map do |href|
15
+ remap_href_path(href, @path)
16
+ end
17
+ end
18
+ end
19
+
20
+ # Not used yet
21
+ def test_parts
22
+ @test_parts ||= @doc.xpath('//xmlns:testPart')
23
+ end
24
+
25
+ # Not used yet
26
+ def assessment_sections
27
+ @assessment_sections ||= test_parts.first.xpath('//xmlns:assessmentSection')
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end