qti 2.19.0 → 2.20.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 59b8ff402b8251fba645448e0c6159d73cbfbaa383d7a796c379136ba5b957bc
4
- data.tar.gz: 9b3d848749f95e31dc095c0b3d8b6eddafdc4eb2eed7fc775b5c3d34a696e56e
3
+ metadata.gz: 1c38dab5972627b0bf1cd428bbde63a1363365dab50bf9726f6b8a35aa605a2a
4
+ data.tar.gz: b00506a2ada125d8853f42fa7b5d328b68985b9821be063e5371a03f6333ef90
5
5
  SHA512:
6
- metadata.gz: 9487c787ace952ccb0771573bf7293c3797982c411f3c21af35bd57fca53b1a956cf593650aff155af2ea29471e59efa7586bf89a39cf9485396790d62ecc37e
7
- data.tar.gz: c23ecddd40c5dc43e7078b66e569658fa34e25e5511d4999252b9e81cd0bc72ddb098fa64c1f99b6dcf5056ed38b40150b385e6ac6cfad8ca3a77081e9899490
6
+ metadata.gz: a4ac85f4bc7890cf07b860b9ddeb8874298dd18fc04136c3d5432e4e0513ec07fb15971657f08ef120ffa8040c1361d3d48b4a45457cb261415a8e1ce9b49358
7
+ data.tar.gz: b2445a01bdeb590a23f2f89a050215b4b19290a436d2d49ea14d1d52a2c6be2b9c486e85095d68153ca5ace4c894d3a1524276d0189ccaae4024fffe2a1c5dad
@@ -17,6 +17,10 @@ module Qti
17
17
  @parent_identifier ||= @node.parent.parent.attributes['ident']&.value
18
18
  end
19
19
 
20
+ def position
21
+ @position ||= @node.attributes['position']&.value
22
+ end
23
+
20
24
  def item_body
21
25
  @item_body ||= begin
22
26
  node = @node.dup
@@ -39,19 +39,33 @@ module Qti
39
39
 
40
40
  def blank_id(stem_item)
41
41
  return stem_item unless canvas_custom_fitb?
42
- canvas_blank_id(stem_item)
42
+ return canvas_blank_id(stem_item) unless new_quizzes_fib?
43
+ nq_blank_id(stem_item)
43
44
  end
44
45
 
45
46
  def blank_value(blank_id)
47
+ return nq_blank_value(blank_id) if new_quizzes_fib?
48
+
46
49
  blank = canvas_fib_responses.find { |response| response[:id] == blank_id }
47
50
  correct_choice = blank[:choices].find { |c| c[:id] == correct_choice_id(blank) }
48
51
  (correct_choice || {})[:item_body] || blank&.dig(:choices, 0, :item_body)
49
52
  end
50
53
 
54
+ # The correct answer value is not necessarily the first one in NQ
55
+ def nq_blank_value(blank_id)
56
+ blank = canvas_fib_responses.find { |response| response[:id] == blank_id }
57
+ correct_choice = blank[:choices].find { |choice| choice[:id] == correct_answer_map[blank_id] }
58
+ correct_choice[:item_body]
59
+ end
60
+
51
61
  def canvas_custom_fitb?
52
62
  @canvas_custom_fitb ||= BaseInteraction.canvas_custom_fitb?(@node)
53
63
  end
54
64
 
65
+ def new_quizzes_fib?
66
+ @new_quizzes_fib ||= BaseInteraction.new_quizzes_fib?(@node)
67
+ end
68
+
55
69
  def canvas_blank_id(stem_item)
56
70
  blank_id = nil
57
71
  node.xpath('.//xmlns:response_lid/xmlns:material').children.map do |response_lid_node|
@@ -62,6 +76,14 @@ module Qti
62
76
  blank_id
63
77
  end
64
78
 
79
+ def nq_blank_id(stem_item)
80
+ blank_id = nil
81
+ node.xpath('.//xmlns:response_lid').map do |resp|
82
+ blank_id = resp[:ident] if resp[:ident] == "response_#{stem_item}"
83
+ end
84
+ blank_id
85
+ end
86
+
65
87
  def canvas_fib_responses
66
88
  @base_canvas_blanks ||= node.xpath('.//xmlns:response_lid').map do |resp|
67
89
  index = 0
@@ -80,8 +102,23 @@ module Qti
80
102
  @canvas_fib_response_ids ||= canvas_fib_responses.map { |b| "[#{b[:id].sub(/^response_/, '')}]" }
81
103
  end
82
104
 
105
+ def correct_answer_map
106
+ @correct_answer_map ||= correct_answers
107
+ end
108
+
83
109
  private
84
110
 
111
+ # Maps response ids to their correct answer's id
112
+ def correct_answers
113
+ correct_answers = {}
114
+
115
+ node.xpath('.//xmlns:varequal').each do |correct_answer|
116
+ correct_answers[correct_answer.attributes['respident']&.value] = correct_answer.text
117
+ end
118
+
119
+ correct_answers
120
+ end
121
+
85
122
  def correct_choice_id(blank)
86
123
  node.xpath('.//xmlns:resprocessing/xmlns:respcondition/xmlns:conditionvar/xmlns:varequal')
87
124
  .find { |varequal| varequal.attribute('respident').value == blank[:id] }&.text
@@ -20,6 +20,15 @@ module Qti
20
20
  question_type(node) == 'fill_in_multiple_blanks_question'
21
21
  end
22
22
 
23
+ def self.new_quizzes_fib?(node)
24
+ return false unless canvas_multiple_fib?(node)
25
+
26
+ first_match = node.at_xpath('.//xmlns:response_label')
27
+ return false if first_match.blank?
28
+
29
+ first_match.attr('scoring_algorithm').present?
30
+ end
31
+
23
32
  def self.question_type(node)
24
33
  path = './/xmlns:qtimetadatafield/xmlns:fieldlabel' \
25
34
  '[text()="question_type"]/../xmlns:fieldentry'
@@ -30,6 +30,10 @@ module Qti
30
30
  @canvas_multiple_fib ||= BaseInteraction.canvas_multiple_fib?(@node)
31
31
  end
32
32
 
33
+ def new_quizzes_fib?
34
+ @new_quizzes_fib ||= BaseInteraction.new_quizzes_fib?(@node)
35
+ end
36
+
33
37
  def stem_items
34
38
  if canvas_multiple_fib?
35
39
  canvas_stem_items(node.at_xpath('.//xmlns:mattext').text)
@@ -76,17 +80,55 @@ module Qti
76
80
  end
77
81
 
78
82
  def scoring_data_structs
79
- answer_nodes.map do |value_node|
83
+ @scoring_data_structs ||= answer_nodes.map do |value_node|
80
84
  ScoringData.new(
81
85
  value_node.content,
82
86
  rcardinality,
83
87
  id: scoring_data_id(value_node),
84
88
  case: scoring_data_case(value_node),
85
- parent_identifier: value_node.parent.parent.attributes['ident']&.value
89
+ parent_identifier: value_node.parent.parent.attributes['ident']&.value,
90
+ scoring_algorithm: scoring_data_algorithm(value_node),
91
+ answer_type: scoring_data_answer_type(value_node),
92
+ scoring_options: scoring_data_options(value_node)
86
93
  )
87
94
  end
88
95
  end
89
96
 
97
+ def wordbank_answer_present?
98
+ return false unless first_wordbank_answer
99
+
100
+ true
101
+ end
102
+
103
+ def wordbank_allow_reuse?
104
+ return unless wordbank_answer_present?
105
+
106
+ wordbank_struct = scoring_data_structs.find { |struct| struct.answer_type == 'wordbank' }
107
+ return false unless wordbank_struct.scoring_options['allow_reuse'] == 'true'
108
+
109
+ true
110
+ end
111
+
112
+ def wordbank_choices
113
+ return unless wordbank_answer_present?
114
+
115
+ choices = []
116
+
117
+ wordbank_answers = all_wordbank_answers.map do |node|
118
+ V1::Models::Choices::FillBlankChoice.new(node, self)
119
+ end
120
+
121
+ wordbank_answers.each do |wordbank_answer|
122
+ choices.push({ id: wordbank_answer.identifier, item_body: wordbank_answer.item_body })
123
+ end
124
+
125
+ choices
126
+ end
127
+
128
+ def correct_answer_map
129
+ @correct_answer_map ||= super
130
+ end
131
+
90
132
  private
91
133
 
92
134
  def rcardinality
@@ -110,6 +152,45 @@ module Qti
110
152
  node.attributes['case']&.value || 'no'
111
153
  end
112
154
 
155
+ def scoring_data_algorithm(node)
156
+ node.attributes['scoring_algorithm']&.value || 'TextInChoices'
157
+ end
158
+
159
+ def scoring_data_answer_type(node)
160
+ node.attributes['answer_type']&.value || 'openEntry'
161
+ end
162
+
163
+ def scoring_data_options(node)
164
+ options = {}
165
+ algorithm = scoring_data_algorithm(node)
166
+ type = scoring_data_answer_type(node)
167
+
168
+ if algorithm == 'TextCloseEnough'
169
+ options['levenshtein_distance'] = levenshtein_distance(node)
170
+ options['ignore_case'] = ignore_case(node)
171
+ end
172
+
173
+ options['allow_reuse'] = node.attributes['allow_reuse']&.value if type == 'wordbank'
174
+
175
+ options
176
+ end
177
+
178
+ def levenshtein_distance(node)
179
+ node.attributes['levenshtein_distance']&.value || '1'
180
+ end
181
+
182
+ def ignore_case(node)
183
+ node.attributes['ignore_case']&.value || 'false'
184
+ end
185
+
186
+ def first_wordbank_answer
187
+ @first_wordbank_answer ||= answer_nodes.find { |node| scoring_data_answer_type(node) == 'wordbank' }
188
+ end
189
+
190
+ def all_wordbank_answers
191
+ @all_wordbank_answers ||= answer_nodes.find_all { |node| scoring_data_answer_type(node) == 'wordbank' }
192
+ end
193
+
113
194
  def qti_stem_item(index, stem_item)
114
195
  if stem_item.xpath('./xmlns:render_fib').present?
115
196
  blank_id = stem_item.attributes['ident'].value
@@ -1,9 +1,11 @@
1
1
  module Qti
2
2
  module V1
3
3
  module Models
4
- ScoringData = Struct.new(:values, :rcardinality, :id, :case, :parent_identifier) do
4
+ ScoringData = Struct.new(:values, :rcardinality, :id, :case, :parent_identifier, :scoring_algorithm,
5
+ :answer_type, :scoring_options) do
5
6
  def initialize(values, rcardinality, options = {})
6
- super(values, rcardinality, options[:id], options[:case], options[:parent_identifier])
7
+ super(values, rcardinality, options[:id], options[:case], options[:parent_identifier],
8
+ options[:scoring_algorithm], options[:answer_type], options[:scoring_options])
7
9
  end
8
10
  end
9
11
  end
data/lib/qti/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Qti
2
- VERSION = '2.19.0'.freeze
2
+ VERSION = '2.20.0'.freeze
3
3
  end
@@ -0,0 +1,86 @@
1
+ <?xml version="1.0" encoding="ISO-8859-1"?>
2
+ <questestinterop xmlns="http://www.imsglobal.org/xsd/ims_qtiasiv1p2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.imsglobal.org/xsd/ims_qtiasiv1p2 http://www.imsglobal.org/xsd/ims_qtiasiv1p2p1.xsd">
3
+ <item ident="a7ca973956582afc57a81192b444632c" title="NQ Fill in Multiple Blanks Answer Types">
4
+ <itemmetadata>
5
+ <qtimetadata>
6
+ <qtimetadatafield>
7
+ <fieldlabel>question_type</fieldlabel>
8
+ <fieldentry>fill_in_multiple_blanks_question</fieldentry>
9
+ </qtimetadatafield>
10
+ <qtimetadatafield>
11
+ <fieldlabel>points_possible</fieldlabel>
12
+ <fieldentry>1.0</fieldentry>
13
+ </qtimetadatafield>
14
+ <qtimetadatafield>
15
+ <fieldlabel>original_answer_ids</fieldlabel>
16
+ <fieldentry>["378646a8-b823-4e5b-beb8-19ca63f1f355", "c9400bfb-78ea-45b6-b5a5-e3311f6d5ed0", "76350749-49ac-4094-966b-c4e1f12d54bc", "cd11d826-906d-4dc4-b78d-d66516eb94ce", "bae0bd4f-2327-4a3e-b29f-199e1e279e91"]</fieldentry>
17
+ </qtimetadatafield>
18
+ <qtimetadatafield>
19
+ <fieldlabel>assessment_question_identifierref</fieldlabel>
20
+ <fieldentry>1c4b07be8a149dfa8f6a263a548cdd84</fieldentry>
21
+ </qtimetadatafield>
22
+ </qtimetadata>
23
+ </itemmetadata>
24
+ <presentation>
25
+ <material>
26
+ <mattext texttype="text/html">&lt;p&gt;Roses are [a20629ed-0c0b-4959-b565-a80c0f199602] and [ab37a945-ebad-4787-a356-66c3c91efcc6].&lt;/p&gt;</mattext>
27
+ </material>
28
+ <response_lid ident="response_a20629ed-0c0b-4959-b565-a80c0f199602">
29
+ <material>
30
+ <mattext>black</mattext>
31
+ </material>
32
+ <render_choice>
33
+ <response_label ident="378646a8-b823-4e5b-beb8-19ca63f1f355" position="1" scoring_algorithm='Equivalence' answer_type='dropdown'>
34
+ <material>
35
+ <mattext texttype="text/plain">black</mattext>
36
+ </material>
37
+ </response_label>
38
+ <response_label ident="c9400bfb-78ea-45b6-b5a5-e3311f6d5ed0" position="2" scoring_algorithm='Equivalence' answer_type='dropdown'>
39
+ <material>
40
+ <mattext texttype="text/plain">violet</mattext>
41
+ </material>
42
+ </response_label>
43
+ <response_label ident="76350749-49ac-4094-966b-c4e1f12d54bc" position="3" scoring_algorithm='Equivalence' answer_type='dropdown'>
44
+ <material>
45
+ <mattext texttype="text/plain">grey</mattext>
46
+ </material>
47
+ </response_label>
48
+ </render_choice>
49
+ </response_lid>
50
+ <response_lid ident="response_ab37a945-ebad-4787-a356-66c3c91efcc6">
51
+ <material>
52
+ <mattext>white</mattext>
53
+ </material>
54
+ <render_choice>
55
+ <response_label ident="cd11d826-906d-4dc4-b78d-d66516eb94ce" scoring_algorithm='TextEquivalence' answer_type='wordbank' allow_reuse='true'>
56
+ <material>
57
+ <mattext texttype="text/plain">brown</mattext>
58
+ </material>
59
+ </response_label>
60
+ <response_label ident="bae0bd4f-2327-4a3e-b29f-199e1e279e91" scoring_algorithm='TextEquivalence' answer_type='wordbank' allow_reuse='true'>
61
+ <material>
62
+ <mattext texttype="text/plain">white</mattext>
63
+ </material>
64
+ </response_label>
65
+ </render_choice>
66
+ </response_lid>
67
+ </presentation>
68
+ <resprocessing>
69
+ <outcomes>
70
+ <decvar maxvalue="100" minvalue="0" varname="SCORE" vartype="Decimal"/>
71
+ </outcomes>
72
+ <respcondition>
73
+ <conditionvar>
74
+ <varequal respident="response_a20629ed-0c0b-4959-b565-a80c0f199602">378646a8-b823-4e5b-beb8-19ca63f1f355</varequal>
75
+ </conditionvar>
76
+ <setvar action="Add" varname="SCORE">50.00</setvar>
77
+ </respcondition>
78
+ <respcondition>
79
+ <conditionvar>
80
+ <varequal respident="response_ab37a945-ebad-4787-a356-66c3c91efcc6">bae0bd4f-2327-4a3e-b29f-199e1e279e91</varequal>
81
+ </conditionvar>
82
+ <setvar action="Add" varname="SCORE">50.00</setvar>
83
+ </respcondition>
84
+ </resprocessing>
85
+ </item>
86
+ </questestinterop>
@@ -0,0 +1,130 @@
1
+ <?xml version="1.0" encoding="ISO-8859-1"?>
2
+ <questestinterop xmlns="http://www.imsglobal.org/xsd/ims_qtiasiv1p2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.imsglobal.org/xsd/ims_qtiasiv1p2 http://www.imsglobal.org/xsd/ims_qtiasiv1p2p1.xsd">
3
+ <item ident="97b0baae152f137d14503e2a2125c4d0" title="NQ Fill in Multiple Blanks Scoring Algorithms">
4
+ <itemmetadata>
5
+ <qtimetadata>
6
+ <qtimetadatafield>
7
+ <fieldlabel>question_type</fieldlabel>
8
+ <fieldentry>fill_in_multiple_blanks_question</fieldentry>
9
+ </qtimetadatafield>
10
+ <qtimetadatafield>
11
+ <fieldlabel>points_possible</fieldlabel>
12
+ <fieldentry>1.0</fieldentry>
13
+ </qtimetadatafield>
14
+ <qtimetadatafield>
15
+ <fieldlabel>original_answer_ids</fieldlabel>
16
+ <fieldentry>["bafd8242-ecdb-4158-a6d3-4ff15e016cb8-0", "d719aee0-6ce0-462c-9c0d-be63ba8d3408-0", "ce47aaf6-c1dd-4db8-9a9f-7f5c131d8a90-0", "41dfd716-8fd9-466a-97fa-33d353e44b42-0", "41dfd716-8fd9-466a-97fa-33d353e44b42-1", "a053119f-6a61-4372-ac79-4b2a7de0232f-0"]</fieldentry>
17
+ </qtimetadatafield>
18
+ <qtimetadatafield>
19
+ <fieldlabel>assessment_question_identifierref</fieldlabel>
20
+ <fieldentry>eaeec2d07f83998ee7b317ffde86dbc0</fieldentry>
21
+ </qtimetadatafield>
22
+ </qtimetadata>
23
+ </itemmetadata>
24
+ <presentation>
25
+ <material>
26
+ <mattext texttype="text/html">&lt;p&gt;Roses are [bafd8242-ecdb-4158-a6d3-4ff15e016cb8], [d719aee0-6ce0-462c-9c0d-be63ba8d3408], [ce47aaf6-c1dd-4db8-9a9f-7f5c131d8a90], [41dfd716-8fd9-466a-97fa-33d353e44b42], and [a053119f-6a61-4372-ac79-4b2a7de0232f].&lt;/p&gt;</mattext>
27
+ </material>
28
+ <response_lid ident="response_bafd8242-ecdb-4158-a6d3-4ff15e016cb8">
29
+ <material>
30
+ <mattext>red</mattext>
31
+ </material>
32
+ <render_choice>
33
+ <response_label ident="bafd8242-ecdb-4158-a6d3-4ff15e016cb8-0" scoring_algorithm='TextContainsAnswer' answer_type='openEntry'>
34
+ <material>
35
+ <mattext texttype="text/plain">red</mattext>
36
+ </material>
37
+ </response_label>
38
+ </render_choice>
39
+ </response_lid>
40
+ <response_lid ident="response_d719aee0-6ce0-462c-9c0d-be63ba8d3408">
41
+ <material>
42
+ <mattext>blue</mattext>
43
+ </material>
44
+ <render_choice>
45
+ <response_label ident="d719aee0-6ce0-462c-9c0d-be63ba8d3408-0" scoring_algorithm='TextCloseEnough' answer_type='openEntry' levenshtein_distance='1' ignore_case='true'>
46
+ <material>
47
+ <mattext texttype="text/plain">blue</mattext>
48
+ </material>
49
+ </response_label>
50
+ </render_choice>
51
+ </response_lid>
52
+ <response_lid ident="response_ce47aaf6-c1dd-4db8-9a9f-7f5c131d8a90">
53
+ <material>
54
+ <mattext>green</mattext>
55
+ </material>
56
+ <render_choice>
57
+ <response_label ident="ce47aaf6-c1dd-4db8-9a9f-7f5c131d8a90-0" scoring_algorithm='TextEquivalence' answer_type='openEntry'>
58
+ <material>
59
+ <mattext texttype="text/plain">green</mattext>
60
+ </material>
61
+ </response_label>
62
+ </render_choice>
63
+ </response_lid>
64
+ <response_lid ident="response_41dfd716-8fd9-466a-97fa-33d353e44b42">
65
+ <material>
66
+ <mattext>yellow</mattext>
67
+ </material>
68
+ <render_choice>
69
+ <response_label ident="41dfd716-8fd9-466a-97fa-33d353e44b42-0" scoring_algorithm='TextInChoices' answer_type='openEntry'>
70
+ <material>
71
+ <mattext texttype="text/plain">yellow</mattext>
72
+ </material>
73
+ </response_label>
74
+ <response_label ident="41dfd716-8fd9-466a-97fa-33d353e44b42-1" scoring_algorithm='TextInChoices' answer_type='openEntry'>
75
+ <material>
76
+ <mattext texttype="text/plain">teal</mattext>
77
+ </material>
78
+ </response_label>
79
+ </render_choice>
80
+ </response_lid>
81
+ <response_lid ident="response_a053119f-6a61-4372-ac79-4b2a7de0232f">
82
+ <material>
83
+ <mattext>^orange$</mattext>
84
+ </material>
85
+ <render_choice>
86
+ <response_label ident="a053119f-6a61-4372-ac79-4b2a7de0232f-0" scoring_algorithm='TextRegex' answer_type='openEntry'>
87
+ <material>
88
+ <mattext texttype="text/plain">^orange$</mattext>
89
+ </material>
90
+ </response_label>
91
+ </render_choice>
92
+ </response_lid>
93
+ </presentation>
94
+ <resprocessing>
95
+ <outcomes>
96
+ <decvar maxvalue="100" minvalue="0" varname="SCORE" vartype="Decimal"/>
97
+ </outcomes>
98
+ <respcondition>
99
+ <conditionvar>
100
+ <varequal respident="response_bafd8242-ecdb-4158-a6d3-4ff15e016cb8">bafd8242-ecdb-4158-a6d3-4ff15e016cb8-0</varequal>
101
+ </conditionvar>
102
+ <setvar action="Add" varname="SCORE">20.00</setvar>
103
+ </respcondition>
104
+ <respcondition>
105
+ <conditionvar>
106
+ <varequal respident="response_d719aee0-6ce0-462c-9c0d-be63ba8d3408">d719aee0-6ce0-462c-9c0d-be63ba8d3408-0</varequal>
107
+ </conditionvar>
108
+ <setvar action="Add" varname="SCORE">20.00</setvar>
109
+ </respcondition>
110
+ <respcondition>
111
+ <conditionvar>
112
+ <varequal respident="response_ce47aaf6-c1dd-4db8-9a9f-7f5c131d8a90">ce47aaf6-c1dd-4db8-9a9f-7f5c131d8a90-0</varequal>
113
+ </conditionvar>
114
+ <setvar action="Add" varname="SCORE">20.00</setvar>
115
+ </respcondition>
116
+ <respcondition>
117
+ <conditionvar>
118
+ <varequal respident="response_41dfd716-8fd9-466a-97fa-33d353e44b42">41dfd716-8fd9-466a-97fa-33d353e44b42-0</varequal>
119
+ </conditionvar>
120
+ <setvar action="Add" varname="SCORE">20.00</setvar>
121
+ </respcondition>
122
+ <respcondition>
123
+ <conditionvar>
124
+ <varequal respident="response_a053119f-6a61-4372-ac79-4b2a7de0232f">a053119f-6a61-4372-ac79-4b2a7de0232f-0</varequal>
125
+ </conditionvar>
126
+ <setvar action="Add" varname="SCORE">20.00</setvar>
127
+ </respcondition>
128
+ </resprocessing>
129
+ </item>
130
+ </questestinterop>
@@ -18,4 +18,18 @@ describe Qti::V1::Models::Choices::FillBlankChoice do
18
18
  expect(choices.map(&:item_body).map { |text| text.include? 'correct' }.any?).to eq false
19
19
  end
20
20
  end
21
+
22
+ context 'nq_multiple_fib_answer_types.xml' do
23
+ let(:fixtures_path) { File.join('spec', 'fixtures') }
24
+ let(:file_path) { File.join(fixtures_path, 'items_1.2', 'nq_multiple_fib_answer_types.xml') }
25
+ let(:test_object) { Qti::V1::Models::Assessment.from_path!(file_path) }
26
+ let(:nodes) { test_object.assessment_items }
27
+ let(:dropdown_answer) { nodes.xpath('.//xmlns:response_lid').first }
28
+ let(:dropdown_choices) { dropdown_answer.xpath('.//xmlns:response_label') }
29
+ let(:choices) { dropdown_choices.map { |answer| described_class.new(answer, test_object) } }
30
+
31
+ it 'returns the right positions' do
32
+ expect(choices.map(&:position)).to eq %w[1 2 3]
33
+ end
34
+ end
21
35
  end
@@ -94,4 +94,20 @@ describe Qti::V1::Models::Interactions::BaseInteraction do
94
94
  include_examples('item_level_feedback')
95
95
  include_examples('answer_feedback')
96
96
  end
97
+
98
+ context 'nq_multiple_fib_scoring_algorithms.xml' do
99
+ let(:file_path) { File.join(fixtures_path, 'nq_multiple_fib_scoring_algorithms.xml') }
100
+
101
+ it 'correctly finds new quizzes QTI' do
102
+ expect(Qti::V1::Models::Interactions::BaseInteraction.new_quizzes_fib?(assessment_item_refs.first)).to eq true
103
+ end
104
+ end
105
+
106
+ context 'canvas_multiple_fib.xml' do
107
+ let(:file_path) { File.join(fixtures_path, 'canvas_multiple_fib.xml') }
108
+
109
+ it 'correctly does not find new quizzes QTI' do
110
+ expect(Qti::V1::Models::Interactions::BaseInteraction.new_quizzes_fib?(assessment_item_refs.first)).to eq false
111
+ end
112
+ end
97
113
  end
@@ -31,6 +31,14 @@ describe Qti::V1::Models::Interactions::FillBlankInteraction do
31
31
  end
32
32
  end
33
33
 
34
+ shared_examples_for 'nq_scoring_data_structs' do
35
+ it 'returns the scoring_data_structs' do
36
+ expect(loaded_class.scoring_data_structs.map(&:scoring_algorithm)).to eq(scoring_data_algorithm)
37
+ expect(loaded_class.scoring_data_structs.map(&:answer_type)).to eq(scoring_data_answer_type)
38
+ expect(loaded_class.scoring_data_structs.map(&:scoring_options)).to eq(scoring_data_options)
39
+ end
40
+ end
41
+
34
42
  shared_examples_for 'stem_items' do
35
43
  it 'returns the stem_items' do
36
44
  expect(loaded_class.stem_items).to eq(expected_stem_items)
@@ -151,6 +159,28 @@ describe Qti::V1::Models::Interactions::FillBlankInteraction do
151
159
  allow(subject).to receive(:answer_nodes).and_return([node])
152
160
  expect(subject.scoring_data_structs.first.case).to eq 'no'
153
161
  end
162
+
163
+ it "returns 'TextInChoices' as scoring algorithm default value" do
164
+ allow(nodexml).to receive(:at_xpath)
165
+ node = double(
166
+ parent: double(parent: double(attributes: 'a')),
167
+ content: 'content',
168
+ attributes: { respident: double(value: 'a') }
169
+ )
170
+ allow(subject).to receive(:answer_nodes).and_return([node])
171
+ expect(subject.scoring_data_structs.first.scoring_algorithm).to eq 'TextInChoices'
172
+ end
173
+
174
+ it "returns 'openEntry' as answer type default value" do
175
+ allow(nodexml).to receive(:at_xpath)
176
+ node = double(
177
+ parent: double(parent: double(attributes: 'a')),
178
+ content: 'content',
179
+ attributes: { respident: double(value: 'a') }
180
+ )
181
+ allow(subject).to receive(:answer_nodes).and_return([node])
182
+ expect(subject.scoring_data_structs.first.answer_type).to eq 'openEntry'
183
+ end
154
184
  end
155
185
 
156
186
  context 'fib_str.xml' do
@@ -192,22 +222,152 @@ describe Qti::V1::Models::Interactions::FillBlankInteraction do
192
222
  let(:scoring_data_case) { %w[no] }
193
223
  let(:answer_count) { 1 }
194
224
  let(:expected_blanks) { [{ id: '3537' }] }
195
- let(:expected_stem_items) do
196
- [
197
- { id: 'stem_0', position: 1, type: 'text', value: '<div><p>Bird, bird, bird, bird is the ' },
198
- { id: 'stem_1', position: 2, type: 'blank', blank_id: 'response_blank1', blank_name: 'word' },
199
- { id: 'stem_2', position: 3, type: 'text', value: '</p></div>' }
200
- ]
201
- end
202
225
 
203
226
  include_examples 'shuffled?'
204
227
  include_examples 'answers'
205
228
  include_examples 'scoring_data_structs'
206
229
  include_examples 'blanks'
207
- include_examples 'stem_items'
208
230
 
209
231
  it 'returns false for #single_fill_in_blank?' do
210
232
  expect(loaded_class.single_fill_in_blank?).to eq false
211
233
  end
212
234
  end
235
+
236
+ context 'new quizzes multiple fill in the blank questions with all possible scoring algorithms' do
237
+ let(:file_path) { File.join(fixtures_path, 'nq_multiple_fib_scoring_algorithms.xml') }
238
+ let(:scoring_data_ids) do
239
+ %w[bafd8242-ecdb-4158-a6d3-4ff15e016cb8-0
240
+ d719aee0-6ce0-462c-9c0d-be63ba8d3408-0
241
+ ce47aaf6-c1dd-4db8-9a9f-7f5c131d8a90-0
242
+ 41dfd716-8fd9-466a-97fa-33d353e44b42-0
243
+ 41dfd716-8fd9-466a-97fa-33d353e44b42-1
244
+ a053119f-6a61-4372-ac79-4b2a7de0232f-0]
245
+ end
246
+ let(:scoring_data_values) { %w[red blue green yellow teal ^orange$] }
247
+ let(:scoring_data_case) { %w[no no no no no no] }
248
+ let(:scoring_data_algorithm) do
249
+ %w[TextContainsAnswer TextCloseEnough TextEquivalence TextInChoices TextInChoices TextRegex]
250
+ end
251
+ let(:scoring_data_answer_type) { %w[openEntry openEntry openEntry openEntry openEntry openEntry] }
252
+ let(:scoring_data_options) { [{}, { 'ignore_case' => 'true', 'levenshtein_distance' => '1' }, {}, {}, {}, {}] }
253
+ let(:correct_answer_map) do
254
+ { 'response_bafd8242-ecdb-4158-a6d3-4ff15e016cb8' => 'bafd8242-ecdb-4158-a6d3-4ff15e016cb8-0',
255
+ 'response_d719aee0-6ce0-462c-9c0d-be63ba8d3408' => 'd719aee0-6ce0-462c-9c0d-be63ba8d3408-0',
256
+ 'response_ce47aaf6-c1dd-4db8-9a9f-7f5c131d8a90' => 'ce47aaf6-c1dd-4db8-9a9f-7f5c131d8a90-0',
257
+ 'response_41dfd716-8fd9-466a-97fa-33d353e44b42' => '41dfd716-8fd9-466a-97fa-33d353e44b42-0',
258
+ 'response_a053119f-6a61-4372-ac79-4b2a7de0232f' => 'a053119f-6a61-4372-ac79-4b2a7de0232f-0' }
259
+ end
260
+ let(:expected_stem_items) do
261
+ [{ id: 'stem_0', position: 1, type: 'text', value: '<p>Roses are ' },
262
+ { blank_id: 'response_bafd8242-ecdb-4158-a6d3-4ff15e016cb8',
263
+ blank_name: 'red',
264
+ id: 'stem_1',
265
+ position: 2,
266
+ type: 'blank' },
267
+ { id: 'stem_2', position: 3, type: 'text', value: ', ' },
268
+ { blank_id: 'response_d719aee0-6ce0-462c-9c0d-be63ba8d3408',
269
+ blank_name: 'blue',
270
+ id: 'stem_3',
271
+ position: 4,
272
+ type: 'blank' },
273
+ { id: 'stem_4', position: 5, type: 'text', value: ', ' },
274
+ { blank_id: 'response_ce47aaf6-c1dd-4db8-9a9f-7f5c131d8a90',
275
+ blank_name: 'green',
276
+ id: 'stem_5',
277
+ position: 6,
278
+ type: 'blank' },
279
+ { id: 'stem_6', position: 7, type: 'text', value: ', ' },
280
+ { blank_id: 'response_41dfd716-8fd9-466a-97fa-33d353e44b42',
281
+ blank_name: 'yellow',
282
+ id: 'stem_7',
283
+ position: 8,
284
+ type: 'blank' },
285
+ { id: 'stem_8', position: 9, type: 'text', value: ', and ' },
286
+ { blank_id: 'response_a053119f-6a61-4372-ac79-4b2a7de0232f',
287
+ blank_name: '^orange$',
288
+ id: 'stem_9',
289
+ position: 10,
290
+ type: 'blank' },
291
+ { id: 'stem_10', position: 11, type: 'text', value: '.</p>' }]
292
+ end
293
+
294
+ include_examples 'scoring_data_structs'
295
+ include_examples 'nq_scoring_data_structs'
296
+ include_examples 'stem_items'
297
+
298
+ it 'returns false for #wordbank_answer_present?' do
299
+ expect(loaded_class.wordbank_answer_present?).to eq false
300
+ end
301
+
302
+ it 'returns nil for #wordbank_allow_reuse?' do
303
+ expect(loaded_class.wordbank_allow_reuse?).to eq nil
304
+ end
305
+
306
+ it 'returns nil for #wordbank_choices' do
307
+ expect(loaded_class.wordbank_choices).to eq nil
308
+ end
309
+
310
+ it 'returns the answer map for #correct_answer_map' do
311
+ expect(loaded_class.correct_answer_map).to eq(correct_answer_map)
312
+ end
313
+ end
314
+
315
+ context 'new quizzes multiple fill in the blank questions with all possible answer types' do
316
+ let(:file_path) { File.join(fixtures_path, 'nq_multiple_fib_answer_types.xml') }
317
+ let(:scoring_data_ids) do
318
+ %w[378646a8-b823-4e5b-beb8-19ca63f1f355
319
+ c9400bfb-78ea-45b6-b5a5-e3311f6d5ed0
320
+ 76350749-49ac-4094-966b-c4e1f12d54bc
321
+ cd11d826-906d-4dc4-b78d-d66516eb94ce
322
+ bae0bd4f-2327-4a3e-b29f-199e1e279e91]
323
+ end
324
+ let(:scoring_data_values) { %w[black violet grey brown white] }
325
+ let(:scoring_data_case) { %w[no no no no no] }
326
+ let(:scoring_data_algorithm) { %w[Equivalence Equivalence Equivalence TextEquivalence TextEquivalence] }
327
+ let(:scoring_data_answer_type) { %w[dropdown dropdown dropdown wordbank wordbank] }
328
+ let(:scoring_data_options) { [{}, {}, {}, { 'allow_reuse' => 'true' }, { 'allow_reuse' => 'true' }] }
329
+ let(:wordbank_choices) do
330
+ [{ id: 'cd11d826-906d-4dc4-b78d-d66516eb94ce', item_body: 'brown' },
331
+ { id: 'bae0bd4f-2327-4a3e-b29f-199e1e279e91', item_body: 'white' }]
332
+ end
333
+ let(:correct_answer_map) do
334
+ { 'response_a20629ed-0c0b-4959-b565-a80c0f199602' => '378646a8-b823-4e5b-beb8-19ca63f1f355',
335
+ 'response_ab37a945-ebad-4787-a356-66c3c91efcc6' => 'bae0bd4f-2327-4a3e-b29f-199e1e279e91' }
336
+ end
337
+ let(:expected_stem_items) do
338
+ [{ id: 'stem_0', position: 1, type: 'text', value: '<p>Roses are ' },
339
+ { blank_id: 'response_a20629ed-0c0b-4959-b565-a80c0f199602',
340
+ blank_name: 'black',
341
+ id: 'stem_1',
342
+ position: 2,
343
+ type: 'blank' },
344
+ { id: 'stem_2', position: 3, type: 'text', value: ' and ' },
345
+ { blank_id: 'response_ab37a945-ebad-4787-a356-66c3c91efcc6',
346
+ blank_name: 'white',
347
+ id: 'stem_3',
348
+ position: 4,
349
+ type: 'blank' },
350
+ { id: 'stem_4', position: 5, type: 'text', value: '.</p>' }]
351
+ end
352
+
353
+ include_examples 'scoring_data_structs'
354
+ include_examples 'nq_scoring_data_structs'
355
+ include_examples 'stem_items'
356
+
357
+ it 'returns true for #wordbank_answer_present?' do
358
+ expect(loaded_class.wordbank_answer_present?).to eq true
359
+ end
360
+
361
+ it 'returns true for #wordbank_allow_reuse?' do
362
+ expect(loaded_class.wordbank_allow_reuse?).to eq true
363
+ end
364
+
365
+ it 'returns the correct choices for #wordbank_choices' do
366
+ expect(loaded_class.wordbank_choices).to eq(wordbank_choices)
367
+ end
368
+
369
+ it 'returns the answer map for #correct_answer_map' do
370
+ expect(loaded_class.correct_answer_map).to eq(correct_answer_map)
371
+ end
372
+ end
213
373
  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: 2.19.0
4
+ version: 2.20.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adrian Diaz
@@ -12,7 +12,7 @@ authors:
12
12
  autorequire:
13
13
  bindir: bin
14
14
  cert_chain: []
15
- date: 2023-08-01 00:00:00.000000000 Z
15
+ date: 2023-08-03 00:00:00.000000000 Z
16
16
  dependencies:
17
17
  - !ruby/object:Gem::Dependency
18
18
  name: actionview
@@ -417,6 +417,8 @@ files:
417
417
  - spec/fixtures/items_1.2/multiple_answer.xml
418
418
  - spec/fixtures/items_1.2/multiple_answer_canvas.xml
419
419
  - spec/fixtures/items_1.2/multiple_choice.xml
420
+ - spec/fixtures/items_1.2/nq_multiple_fib_answer_types.xml
421
+ - spec/fixtures/items_1.2/nq_multiple_fib_scoring_algorithms.xml
420
422
  - spec/fixtures/items_1.2/numeric_exact_match.xml
421
423
  - spec/fixtures/items_1.2/numeric_margin_error.xml
422
424
  - spec/fixtures/items_1.2/numeric_margin_error_percent.xml
@@ -789,6 +791,8 @@ test_files:
789
791
  - spec/fixtures/items_1.2/multiple_answer.xml
790
792
  - spec/fixtures/items_1.2/multiple_answer_canvas.xml
791
793
  - spec/fixtures/items_1.2/multiple_choice.xml
794
+ - spec/fixtures/items_1.2/nq_multiple_fib_answer_types.xml
795
+ - spec/fixtures/items_1.2/nq_multiple_fib_scoring_algorithms.xml
792
796
  - spec/fixtures/items_1.2/numeric_exact_match.xml
793
797
  - spec/fixtures/items_1.2/numeric_margin_error.xml
794
798
  - spec/fixtures/items_1.2/numeric_margin_error_percent.xml