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