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.
- checksums.yaml +4 -4
- data/lib/qti/models/base.rb +49 -2
- data/lib/qti/v1/models/assessment_item.rb +2 -2
- data/lib/qti/v1/models/choices/fill_blank_choice.rb +3 -2
- data/lib/qti/v1/models/choices/logical_identifier_choice.rb +3 -2
- data/lib/qti/v1/models/interactions/base_interaction.rb +3 -2
- data/lib/qti/v1/models/interactions/choice_interaction.rb +3 -7
- data/lib/qti/v1/models/interactions/fill_blank_interaction.rb +3 -7
- data/lib/qti/v1/models/interactions/match_interaction.rb +4 -4
- data/lib/qti/v1/models/interactions/ordering_interaction.rb +2 -2
- data/lib/qti/v1/models/interactions/string_interaction.rb +2 -6
- data/lib/qti/v1/models/interactions.rb +2 -2
- data/lib/qti/v2/models/assessment_item.rb +1 -1
- data/lib/qti/v2/models/choices/gap_match_choice.rb +3 -2
- data/lib/qti/v2/models/choices/simple_choice.rb +3 -2
- data/lib/qti/v2/models/interactions/base_interaction.rb +2 -1
- data/lib/qti/v2/models/interactions/categorization_interaction.rb +2 -2
- data/lib/qti/v2/models/interactions/choice_interaction.rb +3 -3
- data/lib/qti/v2/models/interactions/extended_text_interaction.rb +2 -2
- data/lib/qti/v2/models/interactions/gap_match_interaction.rb +3 -7
- data/lib/qti/v2/models/interactions/match_interaction.rb +5 -5
- data/lib/qti/v2/models/interactions/match_item_tag_processors/associate_interaction_tag_processor.rb +1 -1
- data/lib/qti/v2/models/interactions/match_item_tag_processors/match_interaction_tag_processor.rb +1 -1
- data/lib/qti/v2/models/interactions/ordering_interaction.rb +2 -2
- data/lib/qti/v2/models/interactions/short_text_interaction.rb +2 -2
- data/lib/qti/v2/models/interactions.rb +2 -2
- data/spec/fixtures/no_assessment_XML/59ee7c9d-e6cc-4d0c-b545-258e20b1a244.html +5 -0
- data/spec/fixtures/no_assessment_XML/59ee7c9d-e6cc-4d0c-b545-258e20b1a244.xml +31 -0
- data/spec/fixtures/no_assessment_XML/59ee7c9d-e6cc-4d0c-b545-258e20b1a244_choice_A.html +3 -0
- data/spec/lib/qti/v1/models/choices/fill_blank_choice_spec.rb +1 -1
- data/spec/lib/qti/v1/models/choices/logical_identifier_choice_spec.rb +1 -1
- data/spec/lib/qti/v1/models/interactions/base_interaction_spec.rb +2 -1
- data/spec/lib/qti/v1/models/interactions/choice_interaction_spec.rb +1 -1
- data/spec/lib/qti/v1/models/interactions/fill_blank_interaction_spec.rb +3 -2
- data/spec/lib/qti/v1/models/interactions/match_interaction_spec.rb +2 -2
- data/spec/lib/qti/v1/models/interactions/ordering_interaction_spec.rb +3 -3
- data/spec/lib/qti/v1/models/interactions/string_interaction_spec.rb +1 -1
- data/spec/lib/qti/v2/models/choices/gap_match_choice_spec.rb +2 -1
- data/spec/lib/qti/v2/models/choices/simple_associable_choice_spec.rb +2 -1
- data/spec/lib/qti/v2/models/choices/simple_choice_spec.rb +2 -1
- data/spec/lib/qti/v2/models/interactions/choice_interaction_spec.rb +2 -1
- data/spec/lib/qti/v2/models/interactions/extended_text_interaction_spec.rb +2 -1
- data/spec/lib/qti/v2/models/interactions/gap_match_interaction_spec.rb +2 -1
- data/spec/lib/qti/v2/models/interactions/ordering_interaction_spec.rb +2 -2
- data/spec/lib/qti/v2/models/interactions/short_text_interaction_spec.rb +1 -1
- data/spec/lib/qti/v2/models/object_element_spec.rb +28 -0
- metadata +11 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7543c5147bd92cd202829811e0f16d1c886ed2e5
|
4
|
+
data.tar.gz: c7c3d6d77a201a55d2dfaf4c04d01aa555e70bfa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8b64ab839a19673eba4f2400a704a613abc8bb2ab9c6dead2b2f0780eb974916133a55c2d5171ab6af0e9540612af6f759922404ca9a12ad39fe4966deddd638
|
7
|
+
data.tar.gz: 372e4dc9624c533bb6566944cfb676b6a463d67fdef2bdb3649639e01d6ae1d99da4369c55dc66b45cb63c5ec8317b9f441ca698ae27893e6919bb312052c76d
|
data/lib/qti/models/base.rb
CHANGED
@@ -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
|
-
|
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').
|
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.
|
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.
|
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').
|
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
|
@@ -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.
|
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.
|
23
|
+
sanitize_content!(node.to_html)
|
23
24
|
end
|
24
25
|
end
|
25
26
|
end
|
@@ -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)
|
@@ -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,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>
|
@@ -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
|
-
|
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.
|
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-
|
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.
|
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
|