qti 0.8.1 → 0.8.2

Sign up to get free protection for your applications and to get access to all the features.
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