qti 0.8.1 → 0.8.2

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/lib/qti/models/base.rb +49 -2
  3. data/lib/qti/v1/models/assessment_item.rb +2 -2
  4. data/lib/qti/v1/models/choices/fill_blank_choice.rb +3 -2
  5. data/lib/qti/v1/models/choices/logical_identifier_choice.rb +3 -2
  6. data/lib/qti/v1/models/interactions/base_interaction.rb +3 -2
  7. data/lib/qti/v1/models/interactions/choice_interaction.rb +3 -7
  8. data/lib/qti/v1/models/interactions/fill_blank_interaction.rb +3 -7
  9. data/lib/qti/v1/models/interactions/match_interaction.rb +4 -4
  10. data/lib/qti/v1/models/interactions/ordering_interaction.rb +2 -2
  11. data/lib/qti/v1/models/interactions/string_interaction.rb +2 -6
  12. data/lib/qti/v1/models/interactions.rb +2 -2
  13. data/lib/qti/v2/models/assessment_item.rb +1 -1
  14. data/lib/qti/v2/models/choices/gap_match_choice.rb +3 -2
  15. data/lib/qti/v2/models/choices/simple_choice.rb +3 -2
  16. data/lib/qti/v2/models/interactions/base_interaction.rb +2 -1
  17. data/lib/qti/v2/models/interactions/categorization_interaction.rb +2 -2
  18. data/lib/qti/v2/models/interactions/choice_interaction.rb +3 -3
  19. data/lib/qti/v2/models/interactions/extended_text_interaction.rb +2 -2
  20. data/lib/qti/v2/models/interactions/gap_match_interaction.rb +3 -7
  21. data/lib/qti/v2/models/interactions/match_interaction.rb +5 -5
  22. data/lib/qti/v2/models/interactions/match_item_tag_processors/associate_interaction_tag_processor.rb +1 -1
  23. data/lib/qti/v2/models/interactions/match_item_tag_processors/match_interaction_tag_processor.rb +1 -1
  24. data/lib/qti/v2/models/interactions/ordering_interaction.rb +2 -2
  25. data/lib/qti/v2/models/interactions/short_text_interaction.rb +2 -2
  26. data/lib/qti/v2/models/interactions.rb +2 -2
  27. data/spec/fixtures/no_assessment_XML/59ee7c9d-e6cc-4d0c-b545-258e20b1a244.html +5 -0
  28. data/spec/fixtures/no_assessment_XML/59ee7c9d-e6cc-4d0c-b545-258e20b1a244.xml +31 -0
  29. data/spec/fixtures/no_assessment_XML/59ee7c9d-e6cc-4d0c-b545-258e20b1a244_choice_A.html +3 -0
  30. data/spec/lib/qti/v1/models/choices/fill_blank_choice_spec.rb +1 -1
  31. data/spec/lib/qti/v1/models/choices/logical_identifier_choice_spec.rb +1 -1
  32. data/spec/lib/qti/v1/models/interactions/base_interaction_spec.rb +2 -1
  33. data/spec/lib/qti/v1/models/interactions/choice_interaction_spec.rb +1 -1
  34. data/spec/lib/qti/v1/models/interactions/fill_blank_interaction_spec.rb +3 -2
  35. data/spec/lib/qti/v1/models/interactions/match_interaction_spec.rb +2 -2
  36. data/spec/lib/qti/v1/models/interactions/ordering_interaction_spec.rb +3 -3
  37. data/spec/lib/qti/v1/models/interactions/string_interaction_spec.rb +1 -1
  38. data/spec/lib/qti/v2/models/choices/gap_match_choice_spec.rb +2 -1
  39. data/spec/lib/qti/v2/models/choices/simple_associable_choice_spec.rb +2 -1
  40. data/spec/lib/qti/v2/models/choices/simple_choice_spec.rb +2 -1
  41. data/spec/lib/qti/v2/models/interactions/choice_interaction_spec.rb +2 -1
  42. data/spec/lib/qti/v2/models/interactions/extended_text_interaction_spec.rb +2 -1
  43. data/spec/lib/qti/v2/models/interactions/gap_match_interaction_spec.rb +2 -1
  44. data/spec/lib/qti/v2/models/interactions/ordering_interaction_spec.rb +2 -2
  45. data/spec/lib/qti/v2/models/interactions/short_text_interaction_spec.rb +1 -1
  46. data/spec/lib/qti/v2/models/object_element_spec.rb +28 -0
  47. metadata +11 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5e8424c7b1aa08d8050b0ed6fb82a8bfee2e7273
4
- data.tar.gz: 3c2364dbb089fe7d93ba4f371694f022c09aa483
3
+ metadata.gz: 7543c5147bd92cd202829811e0f16d1c886ed2e5
4
+ data.tar.gz: c7c3d6d77a201a55d2dfaf4c04d01aa555e70bfa
5
5
  SHA512:
6
- metadata.gz: 47aa300244b6eb1b7bb9da7a195d14a5d504116806afdd7ef2272d764b2069446e4940df10f853954e5349b1438b4028458353aa75c868a13ffbe378d2159af5
7
- data.tar.gz: efb24fab172368a48d0e52a6e8375cd7961e476b054382819b90a89df3fbf8ab6f83f1853b85cd9f89f0adb0acaa41d3e0082543556210fe8f9cdd61e134577f
6
+ metadata.gz: 8b64ab839a19673eba4f2400a704a613abc8bb2ab9c6dead2b2f0780eb974916133a55c2d5171ab6af0e9540612af6f759922404ca9a12ad39fe4966deddd638
7
+ data.tar.gz: 372e4dc9624c533bb6566944cfb676b6a463d67fdef2bdb3649639e01d6ae1d99da4369c55dc66b45cb63c5ec8317b9f441ca698ae27893e6919bb312052c76d
@@ -23,6 +23,14 @@ module Qti
23
23
  Sanitize.fragment(html, sanitize_config)
24
24
  end
25
25
 
26
+ def object_tag_transformer
27
+ lambda do |env|
28
+ return unless env[:node_name] == 'object'
29
+ return if env[:is_whitelisted] || !env[:node].element?
30
+ replace_object_node(env[:node])
31
+ end
32
+ end
33
+
26
34
  def remap_unknown_tags_transformer
27
35
  lambda do |env|
28
36
  node_name = env[:node_name]
@@ -37,8 +45,11 @@ module Qti
37
45
  end
38
46
  end
39
47
 
40
- def sanitize_config
41
- Sanitize::Config::RELAXED.merge transformers: remap_unknown_tags_transformer
48
+ def sanitize_config(import_objects: true)
49
+ transformers = []
50
+ transformers << object_tag_transformer if import_objects
51
+ transformers << remap_unknown_tags_transformer
52
+ Sanitize::Config::RELAXED.merge transformers: transformers
42
53
  end
43
54
 
44
55
  def self.from_path!(path, package_root = nil)
@@ -90,6 +101,42 @@ module Qti
90
101
  return unless @package_root
91
102
  @package_root = Pathname.new(@package_root).cleanpath.to_s + '/'
92
103
  end
104
+
105
+ def set_paths_from_item(other_item)
106
+ @package_root = other_item.package_root
107
+ @path = other_item.path
108
+ end
109
+
110
+ def replace_object_node(node)
111
+ path = remap_href_path(node[:data])
112
+ if path
113
+ case node[:type]
114
+ when /^image\//
115
+ return replace_with_image(node, node[:data])
116
+ when "text/html"
117
+ return replace_with_html(node, path)
118
+ end
119
+ end
120
+ # remove unrecognized or invalid objects from the sanitized document
121
+ node.unlink
122
+ end
123
+
124
+ def replace_with_image(node, src)
125
+ node.name = 'img'
126
+ node[:src] = src
127
+ end
128
+
129
+ def replace_with_html(node, path)
130
+ begin
131
+ content = File.read(path)
132
+ html_content = Sanitize.fragment(content, sanitize_config(import_objects: false))
133
+ node.name = 'div'
134
+ node.add_child(Nokogiri::HTML.fragment(html_content))
135
+ rescue StandardError => e
136
+ warn "failed to import html object #{path}: #{e.message}"
137
+ node.unlink
138
+ end
139
+ end
93
140
  end
94
141
  end
95
142
  end
@@ -17,7 +17,7 @@ module Qti
17
17
  @item_body ||= begin
18
18
  node = @doc.dup
19
19
  presentation = node.at_xpath('.//xmlns:presentation')
20
- prompt = presentation.at_xpath('.//xmlns:mattext').content
20
+ prompt = presentation.at_xpath('.//xmlns:mattext').to_html
21
21
  sanitize_content!(prompt)
22
22
  end
23
23
  end
@@ -65,7 +65,7 @@ module Qti
65
65
 
66
66
  def interaction_model
67
67
  @interaction_model ||= begin
68
- V1::Models::Interactions.interaction_model(@doc)
68
+ V1::Models::Interactions.interaction_model(@doc, self)
69
69
  end
70
70
  end
71
71
 
@@ -5,8 +5,9 @@ module Qti
5
5
  module Models
6
6
  module Choices
7
7
  class FillBlankChoice < Qti::V1::Models::Base
8
- def initialize(node)
8
+ def initialize(node, parent)
9
9
  @node = node
10
+ set_paths_from_item(parent)
10
11
  end
11
12
 
12
13
  def identifier
@@ -17,7 +18,7 @@ module Qti
17
18
  def item_body
18
19
  @item_body ||= begin
19
20
  node = @node.dup
20
- sanitize_content!(node.content.squish)
21
+ sanitize_content!(node.to_html)
21
22
  end
22
23
  end
23
24
  end
@@ -5,8 +5,9 @@ module Qti
5
5
  module Models
6
6
  module Choices
7
7
  class LogicalIdentifierChoice < Qti::V1::Models::Base
8
- def initialize(node)
8
+ def initialize(node, parent)
9
9
  @node = node
10
+ set_paths_from_item(parent)
10
11
  end
11
12
 
12
13
  def identifier
@@ -16,7 +17,7 @@ module Qti
16
17
  def item_body
17
18
  @item_body ||= begin
18
19
  node = @node.dup
19
- sanitize_content!(node.content.strip.gsub(/\s+/, ' '))
20
+ sanitize_content!(node.to_html)
20
21
  end
21
22
  end
22
23
  end
@@ -5,12 +5,13 @@ module Qti
5
5
  class BaseInteraction < Qti::V1::Models::Base
6
6
  attr_reader :node
7
7
 
8
- def self.matches(node)
8
+ def self.matches(node, parent)
9
9
  false
10
10
  end
11
11
 
12
- def initialize(node)
12
+ def initialize(node, parent)
13
13
  @node = node
14
+ set_paths_from_item(parent)
14
15
  end
15
16
 
16
17
  def shuffled?
@@ -4,21 +4,17 @@ module Qti
4
4
  module Interactions
5
5
  class ChoiceInteraction < BaseInteraction
6
6
  # This will know if a class matches
7
- def self.matches(node)
7
+ def self.matches(node, parent)
8
8
  matches = node.xpath('.//xmlns:response_lid')
9
9
  return false if matches.count > 1 || matches.empty?
10
10
  rcardinality = matches.first.attributes['rcardinality']&.value || 'Single'
11
11
  return false if rcardinality == 'Ordered'
12
- new(node)
13
- end
14
-
15
- def initialize(node)
16
- @node = node
12
+ new(node, parent)
17
13
  end
18
14
 
19
15
  def answers
20
16
  @answers ||= answer_nodes.map do |node|
21
- V1::Models::Choices::LogicalIdentifierChoice.new(node)
17
+ V1::Models::Choices::LogicalIdentifierChoice.new(node, self)
22
18
  end
23
19
  end
24
20
 
@@ -4,15 +4,11 @@ module Qti
4
4
  module Interactions
5
5
  class FillBlankInteraction < BaseInteraction
6
6
  # This will know if a class matches
7
- def self.matches(node)
7
+ def self.matches(node, parent)
8
8
  return false if node.xpath('.//xmlns:other').present?
9
9
  matches = node.xpath('.//xmlns:render_fib')
10
10
  return false if matches.empty?
11
- new(node)
12
- end
13
-
14
- def initialize(node)
15
- @node = node
11
+ new(node, parent)
16
12
  end
17
13
 
18
14
  def stem_items
@@ -44,7 +40,7 @@ module Qti
44
40
 
45
41
  def answers
46
42
  @answers ||= answer_nodes.map do |node|
47
- V1::Models::Choices::FillBlankChoice.new(node)
43
+ V1::Models::Choices::FillBlankChoice.new(node, self)
48
44
  end
49
45
  end
50
46
 
@@ -3,21 +3,21 @@ module Qti
3
3
  module Models
4
4
  module Interactions
5
5
  class MatchInteraction < BaseInteraction
6
- def self.matches(node)
6
+ def self.matches(node, parent)
7
7
  matches = node.xpath('.//xmlns:response_lid')
8
8
  return false if matches.count <= 1
9
- new(node)
9
+ new(node, parent)
10
10
  end
11
11
 
12
12
  def answers
13
13
  @answers ||= answer_nodes.map do |node|
14
- V1::Models::Choices::LogicalIdentifierChoice.new(node)
14
+ V1::Models::Choices::LogicalIdentifierChoice.new(node, self)
15
15
  end
16
16
  end
17
17
 
18
18
  def questions
19
19
  node.xpath('.//xmlns:response_lid').map do |lid_node|
20
- item_body = sanitize_content!(lid_node.at_xpath('.//xmlns:mattext').text)
20
+ item_body = sanitize_content!(lid_node.at_xpath('.//xmlns:mattext').to_html)
21
21
  { id: lid_node.attributes['ident'].value, itemBody: item_body }
22
22
  end
23
23
  end
@@ -4,12 +4,12 @@ module Qti
4
4
  module Interactions
5
5
  class OrderingInteraction < ChoiceInteraction
6
6
  # This will know if a class matches
7
- def self.matches(node)
7
+ def self.matches(node, parent)
8
8
  matches = node.xpath('.//xmlns:response_lid')
9
9
  return false if matches.count > 1 || matches.empty?
10
10
  rcardinality = matches.first.attributes['rcardinality']&.value || 'Single'
11
11
  return false if rcardinality != 'Ordered'
12
- new(node)
12
+ new(node, parent)
13
13
  end
14
14
 
15
15
  def scoring_data_structs
@@ -4,15 +4,11 @@ module Qti
4
4
  module Interactions
5
5
  class StringInteraction < BaseInteraction
6
6
  # This will know if a class matches
7
- def self.matches(node)
7
+ def self.matches(node, parent)
8
8
  return false unless node.xpath('.//xmlns:other').present?
9
9
  matches = node.xpath('.//xmlns:render_fib')
10
10
  return false if matches.empty?
11
- new(matches.first)
12
- end
13
-
14
- def initialize(node)
15
- @node = node
11
+ new(matches.first, parent)
16
12
  end
17
13
 
18
14
  private
@@ -6,12 +6,12 @@ module Qti
6
6
  module Models
7
7
  module Interactions
8
8
  # This one finds the correct parsing model based on the provided xml node
9
- def self.interaction_model(node)
9
+ def self.interaction_model(node, parent)
10
10
  subclasses = constants.map { |c| const_get(c) }
11
11
 
12
12
  # Check for matches
13
13
  matches = subclasses.each_with_object([]) do |interaction_class, result|
14
- match = interaction_class.matches(node)
14
+ match = interaction_class.matches(node, parent)
15
15
  result << match if match
16
16
  end
17
17
  raise UnsupportedSchema if matches.size != 1
@@ -39,7 +39,7 @@ module Qti
39
39
 
40
40
  def interaction_model
41
41
  @interaction_model ||= begin
42
- V2::Models::Interactions.interaction_model(@doc)
42
+ V2::Models::Interactions.interaction_model(@doc, self)
43
43
  end
44
44
  end
45
45
 
@@ -6,8 +6,9 @@ module Qti
6
6
  module Choices
7
7
  class GapMatchChoice < Qti::V2::Models::Base
8
8
  PROHIBITED_NODE_NAMES = 'feedbackInline'.freeze
9
- def initialize(node)
9
+ def initialize(node, parent)
10
10
  @node = node
11
+ set_paths_from_item(parent)
11
12
  end
12
13
 
13
14
  def identifier
@@ -18,7 +19,7 @@ module Qti
18
19
  @item_body ||= begin
19
20
  node = @node.dup
20
21
  node.children.filter(PROHIBITED_NODE_NAMES).each(&:unlink)
21
- sanitize_content!(node.content.strip.gsub(/\s+/, ' '))
22
+ sanitize_content!(node.to_html)
22
23
  end
23
24
  end
24
25
  end
@@ -7,8 +7,9 @@ module Qti
7
7
  class SimpleChoice < Qti::V2::Models::Base
8
8
  PROHIBITED_NODE_NAMES = %w(feedbackInline).join(',').freeze
9
9
 
10
- def initialize(node)
10
+ def initialize(node, parent)
11
11
  @node = node
12
+ set_paths_from_item(parent)
12
13
  end
13
14
 
14
15
  def identifier
@@ -19,7 +20,7 @@ module Qti
19
20
  @item_body ||= begin
20
21
  node = @node.dup
21
22
  node.children.filter(PROHIBITED_NODE_NAMES).map(&:unlink)
22
- sanitize_content!(node.content.strip.gsub(/\s+/, ' '))
23
+ sanitize_content!(node.to_html)
23
24
  end
24
25
  end
25
26
  end
@@ -9,8 +9,9 @@ module Qti
9
9
  false
10
10
  end
11
11
 
12
- def initialize(node)
12
+ def initialize(node, parent)
13
13
  @node = node
14
+ set_paths_from_item(parent)
14
15
  end
15
16
 
16
17
  def shuffled?
@@ -5,9 +5,9 @@ module Qti
5
5
  module Models
6
6
  module Interactions
7
7
  class CategorizationInteraction < MatchInteraction
8
- def self.matches(node)
8
+ def self.matches(node, parent)
9
9
  if use_match_interaction_implementation?(node)
10
- new(node, MatchItemTagProcessors::MatchInteractionTagProcessor)
10
+ new(node, parent, MatchItemTagProcessors::MatchInteractionTagProcessor)
11
11
  else
12
12
  false
13
13
  end
@@ -3,17 +3,17 @@ module Qti
3
3
  module Models
4
4
  module Interactions
5
5
  class ChoiceInteraction < BaseInteraction
6
- def self.matches(node)
6
+ def self.matches(node, parent)
7
7
  matches = node.xpath('.//xmlns:choiceInteraction')
8
8
  return false if matches.empty?
9
9
 
10
10
  raise Qti::UnsupportedSchema if matches.size > 1
11
- new(matches.first)
11
+ new(matches.first, parent)
12
12
  end
13
13
 
14
14
  def answers
15
15
  @answers ||= answer_nodes.map do |node|
16
- V2::Models::Choices::SimpleChoice.new(node)
16
+ V2::Models::Choices::SimpleChoice.new(node, self)
17
17
  end
18
18
  end
19
19
 
@@ -4,12 +4,12 @@ module Qti
4
4
  module Interactions
5
5
  class ExtendedTextInteraction < BaseInteraction
6
6
  # This will know if a class matches
7
- def self.matches(node)
7
+ def self.matches(node, parent)
8
8
  matches = node.xpath('.//xmlns:extendedTextInteraction')
9
9
  return false if matches.empty?
10
10
 
11
11
  raise Qti::UnsupportedSchema if matches.size > 1
12
- new(matches.first)
12
+ new(matches.first, parent)
13
13
  end
14
14
 
15
15
  # not used yet
@@ -3,14 +3,10 @@ module Qti
3
3
  module Models
4
4
  module Interactions
5
5
  class GapMatchInteraction < BaseInteraction
6
- def self.matches(node)
6
+ def self.matches(node, parent)
7
7
  matches = node.xpath('.//xmlns:gapMatchInteraction')
8
8
  return false if matches.count != 1
9
- new(node)
10
- end
11
-
12
- def initialize(node)
13
- @node = node
9
+ new(node, parent)
14
10
  end
15
11
 
16
12
  def shuffled?
@@ -86,7 +82,7 @@ module Qti
86
82
 
87
83
  def answers
88
84
  @answers ||= answer_nodes.map do |node|
89
- V2::Models::Choices::GapMatchChoice.new(node)
85
+ V2::Models::Choices::GapMatchChoice.new(node, self)
90
86
  end
91
87
  end
92
88
 
@@ -10,9 +10,9 @@ module Qti
10
10
 
11
11
  attr_reader :implementation
12
12
 
13
- def initialize(node, implementation)
14
- super(node)
15
- @implementation = implementation.new(node)
13
+ def initialize(node, parent, implementation)
14
+ super(node, parent)
15
+ @implementation = implementation.new(node, parent)
16
16
  end
17
17
 
18
18
  def_delegators :@implementation, :answers, :questions, :shuffled?
@@ -21,7 +21,7 @@ module Qti
21
21
  implementation.scoring_data_structs
22
22
  end
23
23
 
24
- def self.matches(node)
24
+ def self.matches(node, parent)
25
25
  implementation =
26
26
  if use_associate_interaction_implementation?(node)
27
27
  MatchItemTagProcessors::AssociateInteractionTagProcessor
@@ -30,7 +30,7 @@ module Qti
30
30
  end
31
31
 
32
32
  return false unless implementation.present?
33
- new(node, implementation)
33
+ new(node, parent, implementation)
34
34
  end
35
35
 
36
36
  def self.use_associate_interaction_implementation?(node)
@@ -28,7 +28,7 @@ module Qti
28
28
  end
29
29
 
30
30
  def answers
31
- answer_nodes.map { |node| Choices::SimpleAssociableChoice.new(node) }
31
+ answer_nodes.map { |node| Choices::SimpleAssociableChoice.new(node, self) }
32
32
  end
33
33
 
34
34
  def scoring_data_structs
@@ -24,7 +24,7 @@ module Qti
24
24
  end
25
25
 
26
26
  def answers
27
- answer_nodes.map { |node| Choices::SimpleAssociableChoice.new(node) }
27
+ answer_nodes.map { |node| Choices::SimpleAssociableChoice.new(node, self) }
28
28
  end
29
29
 
30
30
  def scoring_data_structs
@@ -5,10 +5,10 @@ module Qti
5
5
  module Models
6
6
  module Interactions
7
7
  class OrderingInteraction < ChoiceInteraction
8
- def self.matches(node)
8
+ def self.matches(node, parent)
9
9
  match = node.at_xpath('.//xmlns:orderInteraction')
10
10
  return false unless match.present?
11
- new(node)
11
+ new(node, parent)
12
12
  end
13
13
 
14
14
  def shuffled?
@@ -4,12 +4,12 @@ module Qti
4
4
  module Interactions
5
5
  class ShortTextInteraction < BaseInteraction
6
6
  # This will know if a class matches
7
- def self.matches(node)
7
+ def self.matches(node, parent)
8
8
  matches = node.xpath('.//xmlns:textEntryInteraction')
9
9
  return false if matches.empty?
10
10
 
11
11
  raise Qti::UnsupportedSchema if matches.size > 1
12
- new(matches.first)
12
+ new(matches.first, parent)
13
13
  end
14
14
  end
15
15
  end
@@ -6,10 +6,10 @@ module Qti
6
6
  module Models
7
7
  module Interactions
8
8
  # This one finds the correct parsing model based on the provided xml node
9
- def self.interaction_model(node)
9
+ def self.interaction_model(node, parent)
10
10
  # Check for matches
11
11
  matches = subclasses.each_with_object([]) do |interaction_class, result|
12
- match = interaction_class.matches(node)
12
+ match = interaction_class.matches(node, parent)
13
13
  result << match if match
14
14
  end
15
15
 
@@ -0,0 +1,5 @@
1
+ <p>Listen to the musical example. &nbsp;</p>
2
+ <p><a shape="rect" href="objects/83+Track+83.mp3">83Track83.mp3</a></p>
3
+ <p>Which written notation matches what is heard?&nbsp;&nbsp;&nbsp;</p>
4
+ <p></p>
5
+ <p>&nbsp;</p>
@@ -0,0 +1,31 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <assessmentItem adaptive="false" identifier="choice" timeDependent="false" title="02-1303010-00046" xmlns="http://www.imsglobal.org/xsd/imsqti_v2p1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.imsglobal.org/xsd/imsqti_v2p1 http://www.imsglobal.org/xsd/qti/qtiv2p1/imsqti_v2p1.xsd">
3
+ <responseDeclaration baseType="identifier" cardinality="single" identifier="RESPONSE">
4
+ <correctResponse>
5
+ <value>A</value>
6
+ </correctResponse>
7
+ </responseDeclaration>
8
+ <outcomeDeclaration baseType="identifier" cardinality="single" identifier="FEEDBACK" normalMaximum="1">
9
+ <defaultValue>
10
+ <value>1</value>
11
+ </defaultValue>
12
+ </outcomeDeclaration>
13
+ <itemBody>
14
+ <div>
15
+ <object data="59ee7c9d-e6cc-4d0c-b545-258e20b1a244.html" type="text/html"/>
16
+ </div>
17
+ <choiceInteraction maxChoices="1" responseIdentifier="RESPONSE" shuffle="false">
18
+ <simpleChoice identifier="A">
19
+ <object data="59ee7c9d-e6cc-4d0c-b545-258e20b1a244_choice_A.html" type="text/html"/>
20
+ <feedbackInline identifier="A" outcomeIdentifier="FEEDBACK" showHide="show">Key - This is the correct answer because the pitches in this answer are exactly what was played on the stem audio.</feedbackInline>
21
+ </simpleChoice>
22
+ <simpleChoice identifier="B">
23
+ <object data="some_image.jpg" type="image/jpeg"></object>
24
+ </simpleChoice>
25
+ </choiceInteraction>
26
+ <rubricBlock view="scorer">
27
+ <div/>
28
+ </rubricBlock>
29
+ </itemBody>
30
+ <responseProcessing template="http://www.imsglobal.org/question/qti_v2p1/rptemplates/match_correct"/>
31
+ </assessmentItem>
@@ -0,0 +1,3 @@
1
+ <p>test to ensure this stuff gets sanitized.</p>
2
+ <object data="ridiculous_cycle_attempt" type="text/html"></object>
3
+ <script>alert('pwn3d!')</script>
@@ -6,7 +6,7 @@ describe Qti::V1::Models::Choices::FillBlankChoice do
6
6
  let(:file_path) { File.join(fixtures_path, 'items_1.2', 'fib.xml') }
7
7
  let(:test_object) { Qti::V1::Models::Assessment.from_path!(file_path) }
8
8
  let(:nodes) { test_object.assessment_items }
9
- let(:choices) { nodes.map { |node| described_class.new(node) } }
9
+ let(:choices) { nodes.map { |node| described_class.new(node, test_object) } }
10
10
 
11
11
  it 'returns the right identifier' do
12
12
  expect(choices.map(&:identifier)).to eq %w(FIB_STR)
@@ -6,7 +6,7 @@ describe Qti::V1::Models::Choices::LogicalIdentifierChoice do
6
6
  let(:file_path) { File.join(fixtures_path, 'items_1.2', 'choice.xml') }
7
7
  let(:test_object) { Qti::V1::Models::Assessment.from_path!(file_path) }
8
8
  let(:nodes) { test_object.assessment_items }
9
- let(:choices) { nodes.map { |node| described_class.new(node) } }
9
+ let(:choices) { nodes.map { |node| described_class.new(node, test_object) } }
10
10
 
11
11
  it 'returns the right identifier' do
12
12
  expect(choices.map(&:identifier)).to eq %w(QUE_1003 QUE_1007 QUE_1022)
@@ -12,9 +12,10 @@ describe Qti::V1::Models::Interactions::BaseInteraction do
12
12
  end
13
13
 
14
14
  let(:node) { Nokogiri.XML(doc, &:noblanks) }
15
+ let(:assessment) { double(path: 'dummy/blah', package_root: 'dummy') }
15
16
 
16
17
  it 'returns "Single" rcardinality by default' do
17
- interaction = described_class.new(node)
18
+ interaction = described_class.new(node, assessment)
18
19
  expect(interaction.rcardinality).to eq 'Single'
19
20
  end
20
21
  end
@@ -4,7 +4,7 @@ describe Qti::V1::Models::Interactions::ChoiceInteraction do
4
4
  let(:fixtures_path) { File.join('spec', 'fixtures', 'items_1.2') }
5
5
  let(:test_object) { Qti::V1::Models::Assessment.from_path!(file_path) }
6
6
  let(:assessment_item_refs) { test_object.assessment_items }
7
- let(:loaded_class) { described_class.new(assessment_item_refs.first) }
7
+ let(:loaded_class) { described_class.new(assessment_item_refs.first, test_object) }
8
8
 
9
9
  shared_examples_for 'shuffled?' do
10
10
  it 'returns shuffle setting' do
@@ -4,7 +4,7 @@ describe Qti::V1::Models::Interactions::FillBlankInteraction do
4
4
  let(:fixtures_path) { File.join('spec', 'fixtures', 'items_1.2') }
5
5
  let(:test_object) { Qti::V1::Models::Assessment.from_path!(file_path) }
6
6
  let(:assessment_item_refs) { test_object.assessment_items }
7
- let(:loaded_class) { described_class.new(assessment_item_refs.first) }
7
+ let(:loaded_class) { described_class.new(assessment_item_refs.first, test_object) }
8
8
 
9
9
  shared_examples_for 'shuffled?' do
10
10
  it 'returns shuffle setting' do
@@ -64,7 +64,8 @@ describe Qti::V1::Models::Interactions::FillBlankInteraction do
64
64
 
65
65
  describe '#scoring_data_structs' do
66
66
  let(:nodexml) { double }
67
- subject { described_class.new(nodexml) }
67
+ let(:test_object) { double(package_root: 'dummy', path: 'dummy/blah') }
68
+ subject { described_class.new(nodexml, test_object) }
68
69
 
69
70
  it "returns 'no' as case default value" do
70
71
  allow(nodexml).to receive(:at_xpath)
@@ -4,7 +4,7 @@ describe Qti::V1::Models::Interactions::MatchInteraction do
4
4
  let(:file) { File.join('spec', 'fixtures', 'items_1.2', 'matching.xml') }
5
5
  let(:assessment) { Qti::V1::Models::Assessment.from_path!(file) }
6
6
  let(:item) { assessment.assessment_items.first }
7
- subject { described_class.new(item) }
7
+ subject { described_class.new(item, assessment) }
8
8
 
9
9
  it 'returns shuffle setting' do
10
10
  expect(subject.shuffled?).to eq false
@@ -20,7 +20,7 @@ describe Qti::V1::Models::Interactions::MatchInteraction do
20
20
 
21
21
  describe '.matches' do
22
22
  it 'matches the item in file' do
23
- expect(described_class.matches(item)).to be_truthy
23
+ expect(described_class.matches(item, assessment)).to be_truthy
24
24
  end
25
25
  end
26
26
 
@@ -4,17 +4,17 @@ describe Qti::V1::Models::Interactions::OrderingInteraction do
4
4
  let(:file) { File.join('spec', 'fixtures', 'items_1.2', 'ordering.xml') }
5
5
  let(:assessment) { Qti::V1::Models::Assessment.from_path!(file) }
6
6
  let(:item) { assessment.assessment_items.first }
7
- subject { described_class.new(item) }
7
+ subject { described_class.new(item, assessment) }
8
8
 
9
9
  describe '.matches' do
10
10
  it 'matches the item in file' do
11
- expect(described_class.matches(item)).to be_truthy
11
+ expect(described_class.matches(item, assessment)).to be_truthy
12
12
  end
13
13
  end
14
14
 
15
15
  describe '#scoring_data_structs' do
16
16
  it 'grabs scoring data value for ordering questions' do
17
- assessment_item = described_class.new(assessment.assessment_items.first)
17
+ assessment_item = described_class.new(assessment.assessment_items.first, assessment)
18
18
  expect(assessment_item.scoring_data_structs.map(&:values)).to eq %w(A B E D C)
19
19
  end
20
20
  end
@@ -4,7 +4,7 @@ describe Qti::V1::Models::Interactions::StringInteraction do
4
4
  let(:fixtures_path) { File.join('spec', 'fixtures', 'items_1.2') }
5
5
  let(:test_object) { Qti::V1::Models::Assessment.from_path!(file_path) }
6
6
  let(:assessment_item_refs) { test_object.assessment_items }
7
- let(:loaded_class) { described_class.new(assessment_item_refs.first) }
7
+ let(:loaded_class) { described_class.new(assessment_item_refs.first, test_object) }
8
8
 
9
9
  context 'essay.xml' do
10
10
  let(:file_path) { File.join(fixtures_path, 'essay.xml') }
@@ -2,10 +2,11 @@ require 'spec_helper'
2
2
 
3
3
  describe Qti::V2::Models::Choices::GapMatchChoice do
4
4
  context 'choice.xml' do
5
+ let(:item) { double(package_root: 'dummy', path: 'dummy/blah') }
5
6
  let(:io) { File.read(File.join('spec', 'fixtures', 'items_2.1', 'gap_match.xml')) }
6
7
  let(:nodes) { Nokogiri::XML(io).xpath('//xmlns:gapText') }
7
8
 
8
- let(:choices) { nodes.map { |node| described_class.new(node) } }
9
+ let(:choices) { nodes.map { |node| described_class.new(node, item) } }
9
10
 
10
11
  it 'returns the right identifier' do
11
12
  expect(choices.map(&:identifier)).to eq %w(W Sp Su A)
@@ -1,9 +1,10 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Qti::V2::Models::Choices::SimpleAssociableChoice do
4
+ let(:item) { double(package_root: 'dummy', path: 'dummy/blah') }
4
5
  let(:io) { File.read('spec/fixtures/items_2.1/match2.xml') }
5
6
  let(:nodes) { Nokogiri::XML(io).xpath('//xmlns:simpleAssociableChoice') }
6
- let(:choices) { nodes.map { |node| described_class.new(node) } }
7
+ let(:choices) { nodes.map { |node| described_class.new(node, item) } }
7
8
 
8
9
  it 'returns the expected attributes' do
9
10
  expected_attrs = [
@@ -2,10 +2,11 @@ require 'spec_helper'
2
2
 
3
3
  describe Qti::V2::Models::Choices::SimpleChoice do
4
4
  context 'choice.xml' do
5
+ let(:item) { double(package_root: 'dummy', path: 'dummy/blah') }
5
6
  let(:io) { File.read(File.join('spec', 'fixtures', 'items_2.1', 'Example02-feedbackInline.xml')) }
6
7
  let(:nodes) { Nokogiri::XML(io).xpath('//xmlns:simpleChoice') }
7
8
 
8
- let(:choices) { nodes.map { |node| described_class.new(node) } }
9
+ let(:choices) { nodes.map { |node| described_class.new(node, item) } }
9
10
 
10
11
  it 'returns the right identifier' do
11
12
  expect(choices.map(&:identifier)).to eq %w(true false)
@@ -2,10 +2,11 @@ require 'spec_helper'
2
2
 
3
3
  describe Qti::V2::Models::Interactions::ChoiceInteraction do
4
4
  context 'choice.xml' do
5
+ let(:item) { double(package_root: 'dummy', path: 'dummy/blah') }
5
6
  let(:io) { File.read(File.join('spec', 'fixtures', 'items_2.1', 'choice.xml')) }
6
7
  let(:node) { Nokogiri::XML(io).at_xpath('//xmlns:choiceInteraction') }
7
8
 
8
- let(:loaded_class) { described_class.new(node) }
9
+ let(:loaded_class) { described_class.new(node, item) }
9
10
 
10
11
  it 'returns shuffle setting' do
11
12
  expect(loaded_class.shuffled?).to eq true
@@ -2,10 +2,11 @@ require 'spec_helper'
2
2
 
3
3
  describe Qti::V2::Models::Interactions::ExtendedTextInteraction do
4
4
  context 'essay.xml' do
5
+ let(:item) { double(package_root: 'dummy', path: 'dummy/blah') }
5
6
  let(:io) { File.read(File.join('spec', 'fixtures', 'items_2.1', 'essay.xml')) }
6
7
  let(:node) { Nokogiri::XML(io).at_xpath('//xmlns:extendedTextInteraction') }
7
8
 
8
- let(:loaded_class) { described_class.new(node) }
9
+ let(:loaded_class) { described_class.new(node, item) }
9
10
 
10
11
  it 'returns shuffle setting' do
11
12
  expect(loaded_class.shuffled?).to eq false
@@ -2,9 +2,10 @@ require 'spec_helper'
2
2
 
3
3
  describe Qti::V2::Models::Interactions::GapMatchInteraction do
4
4
  context 'gap_match.xml' do
5
+ let(:item) { double(package_root: 'dummy', path: 'dummy/blah') }
5
6
  let(:io) { File.read(File.join('spec', 'fixtures', 'items_2.1', 'gap_match.xml')) }
6
7
  let(:node) { Nokogiri::XML(io) }
7
- let(:loaded_class) { described_class.new(node) }
8
+ let(:loaded_class) { described_class.new(node, item) }
8
9
 
9
10
  let(:expected_stem_items) {[
10
11
  {type: "text",
@@ -3,11 +3,11 @@ require 'spec_helper'
3
3
  describe Qti::V2::Models::Interactions::OrderingInteraction do
4
4
  let(:file) { File.join('spec', 'fixtures', 'items_2.1', 'order.xml') }
5
5
  let(:item) { Qti::V2::Models::AssessmentItem.from_path!(file) }
6
- subject { described_class.new(item.doc) }
6
+ subject { described_class.new(item.doc, item) }
7
7
 
8
8
  describe '.matches' do
9
9
  it 'matches the item in file' do
10
- expect(described_class.matches(item.doc)).to be_truthy
10
+ expect(described_class.matches(item.doc, item)).to be_truthy
11
11
  end
12
12
  end
13
13
 
@@ -16,7 +16,7 @@ describe Qti::V2::Models::Interactions::ShortTextInteraction do
16
16
  node = double('Nokogiri::XML::Document')
17
17
  matches = [double('Nokogiri::XML::Element'), double('Nokogiri::XML::Element')]
18
18
  allow(node).to receive(:xpath).with('.//xmlns:textEntryInteraction').and_return(matches)
19
- expect{described_class.matches(node)}.to raise_error(Qti::UnsupportedSchema)
19
+ expect{described_class.matches(node, assessment_item)}.to raise_error(Qti::UnsupportedSchema)
20
20
  end
21
21
  end
22
22
  end
@@ -0,0 +1,28 @@
1
+ require 'spec_helper'
2
+
3
+ describe Qti::V2::Models::AssessmentItem do
4
+ context "object element" do
5
+ let(:path) { File.join('spec', 'fixtures', 'no_assessment_XML', '59ee7c9d-e6cc-4d0c-b545-258e20b1a244.xml') }
6
+ let(:loaded_class) { described_class.from_path!(path) }
7
+
8
+ it 'loads an AssessmentItem XML file containing object elements' do
9
+ expect { loaded_class.item_body }.not_to raise_error
10
+ end
11
+
12
+ it 'imports html objects' do
13
+ body = loaded_class.item_body
14
+ expect(body).to include 'Listen to the musical example'
15
+ end
16
+
17
+ it 'sanitizes imported html' do
18
+ body = loaded_class.interaction_model.answers.first.item_body
19
+ expect(body).to include 'test to ensure this stuff gets sanitized'
20
+ expect(body).not_to include '<script>'
21
+ end
22
+
23
+ it 'converts image objects' do
24
+ body = loaded_class.interaction_model.answers.last.item_body
25
+ expect(body).to include '<img src="some_image.jpg">'
26
+ end
27
+ end
28
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: qti
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.1
4
+ version: 0.8.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hannah Bottalla
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2017-08-29 00:00:00.000000000 Z
12
+ date: 2017-09-05 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport
@@ -357,6 +357,9 @@ files:
357
357
  - spec/fixtures/no_assessment_XML/1c7db4f7-e933-4296-8979-963480422832_choice_B.html
358
358
  - spec/fixtures/no_assessment_XML/1c7db4f7-e933-4296-8979-963480422832_choice_C.html
359
359
  - spec/fixtures/no_assessment_XML/1c7db4f7-e933-4296-8979-963480422832_choice_D.html
360
+ - spec/fixtures/no_assessment_XML/59ee7c9d-e6cc-4d0c-b545-258e20b1a244.html
361
+ - spec/fixtures/no_assessment_XML/59ee7c9d-e6cc-4d0c-b545-258e20b1a244.xml
362
+ - spec/fixtures/no_assessment_XML/59ee7c9d-e6cc-4d0c-b545-258e20b1a244_choice_A.html
360
363
  - spec/fixtures/no_assessment_XML/a4f3621a-195c-4847-a72c-69388250a078.html
361
364
  - spec/fixtures/no_assessment_XML/a4f3621a-195c-4847-a72c-69388250a078.xml
362
365
  - spec/fixtures/no_assessment_XML/a4f3621a-195c-4847-a72c-69388250a078_choice_A.html
@@ -484,6 +487,7 @@ files:
484
487
  - spec/lib/qti/v2/models/interactions/ordering_interaction_spec.rb
485
488
  - spec/lib/qti/v2/models/interactions/short_text_interaction_spec.rb
486
489
  - spec/lib/qti/v2/models/non_assessment_test_spec.rb
490
+ - spec/lib/qti/v2/models/object_element_spec.rb
487
491
  - spec/lib/qti/v2/models/stimulus_item_spec.rb
488
492
  - spec/lib/qti_spec.rb
489
493
  - spec/spec_helper.rb
@@ -507,7 +511,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
507
511
  version: '0'
508
512
  requirements: []
509
513
  rubyforge_project:
510
- rubygems_version: 2.5.1
514
+ rubygems_version: 2.5.2
511
515
  signing_key:
512
516
  specification_version: 4
513
517
  summary: QTI 1.2 and 2.1 import and export models
@@ -620,6 +624,9 @@ test_files:
620
624
  - spec/fixtures/no_assessment_XML/1c7db4f7-e933-4296-8979-963480422832_choice_B.html
621
625
  - spec/fixtures/no_assessment_XML/1c7db4f7-e933-4296-8979-963480422832_choice_C.html
622
626
  - spec/fixtures/no_assessment_XML/1c7db4f7-e933-4296-8979-963480422832_choice_D.html
627
+ - spec/fixtures/no_assessment_XML/59ee7c9d-e6cc-4d0c-b545-258e20b1a244.html
628
+ - spec/fixtures/no_assessment_XML/59ee7c9d-e6cc-4d0c-b545-258e20b1a244.xml
629
+ - spec/fixtures/no_assessment_XML/59ee7c9d-e6cc-4d0c-b545-258e20b1a244_choice_A.html
623
630
  - spec/fixtures/no_assessment_XML/a4f3621a-195c-4847-a72c-69388250a078.html
624
631
  - spec/fixtures/no_assessment_XML/a4f3621a-195c-4847-a72c-69388250a078.xml
625
632
  - spec/fixtures/no_assessment_XML/a4f3621a-195c-4847-a72c-69388250a078_choice_A.html
@@ -747,6 +754,7 @@ test_files:
747
754
  - spec/lib/qti/v2/models/interactions/ordering_interaction_spec.rb
748
755
  - spec/lib/qti/v2/models/interactions/short_text_interaction_spec.rb
749
756
  - spec/lib/qti/v2/models/non_assessment_test_spec.rb
757
+ - spec/lib/qti/v2/models/object_element_spec.rb
750
758
  - spec/lib/qti/v2/models/stimulus_item_spec.rb
751
759
  - spec/lib/qti_spec.rb
752
760
  - spec/spec_helper.rb