qti 2.29.0 → 2.30.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: 76e0ccb82cec45bc227f147d2b1d505b4117131e1aa6fbc166553efbbb4e248e
4
- data.tar.gz: 180bb912b8a0d53b398ac62fab21922ae4da140817e86c27abb4783bd843f6ba
3
+ metadata.gz: 5faafa20e505881d4e305e9346f24060883b8cf86b1861f08021f6def9329ea9
4
+ data.tar.gz: 69bbc14fecc3909cfc905102433e2ae1631261cf56fcda2c13035a35583a9682
5
5
  SHA512:
6
- metadata.gz: 8c51e5b108c05d11ec919a5118de0e9d5fdcfed4403defecc353f6d93bb083fb6f281db0fd74d1ff636fafda4982981b47ba618198068a933f5991ffac3ec923
7
- data.tar.gz: 55a94df5be4713653c8db8965ada79ea5daee2f1c0c9fe69a92558be7f6a080e3136ee3f6dc5d193d6dc730b6d2235dabb07135ec1e6d19f6311cfe8675baba1
6
+ metadata.gz: 43f8a769c16f2a2671219c7f2f3b4e25a3c8321dfed55d17035d1b3eab2238dcbae93f8b669e4393971a31c9607cfb3f2ca78ba454538bcfd279673f2f271eae
7
+ data.tar.gz: d51c5c66f01164d227c38ebb3098787096ce1b1257737ffe42ae70db5af39c8e09157b87c6469e7b66599803547c3bf92b6c0c9cacb667ef490e15bec86e27df
@@ -29,8 +29,35 @@ module Qti
29
29
  end
30
30
  end
31
31
 
32
+ # Numeric answers cannot be paired with their comment through a
33
+ # `varequal`, the way the base implementation does it: exact and
34
+ # precision answers nest it inside an `or`, and range answers have no
35
+ # single value to emit one for. Canvas instead puts each answer's
36
+ # `displayfeedback` in the same `respcondition` that carries its
37
+ # `setvar SCORE`, so pair them by that node and key the result on the
38
+ # position within `answer_nodes` -- the same list, in the same order,
39
+ # that `scoring_data_structs` is built from.
40
+ def answer_feedback
41
+ answers = answer_nodes.filter_map.with_index do |answer_node, index|
42
+ numeric_answer_feedback_entry(answer_node, index)
43
+ end
44
+ answers unless answers.empty?
45
+ end
46
+
32
47
  private
33
48
 
49
+ def numeric_answer_feedback_entry(answer_node, index)
50
+ refid = answer_node.xpath('./xmlns:displayfeedback[not (@linkrefid="correct_fb" or ' \
51
+ '@linkrefid="general_incorrect_fb" or @linkrefid="general_fb")]').first&.[](:linkrefid)
52
+ feedback = get_feedback(refid)
53
+ return nil unless feedback
54
+ {
55
+ response_index: index,
56
+ texttype: feedback[:texttype],
57
+ feedback: feedback.text
58
+ }
59
+ end
60
+
34
61
  def answer_nodes
35
62
  @node.xpath('.//xmlns:respcondition/xmlns:setvar[@varname="SCORE"]/..')
36
63
  end
data/lib/qti/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Qti
2
- VERSION = '2.29.0'.freeze
2
+ VERSION = '2.30.0'.freeze
3
3
  end
@@ -0,0 +1,189 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!--
3
+ Numeric per-answer feedback fixture. Question1 covers all four numeric answer
4
+ types (exact, precision, margin of error, range) plus item level feedback that
5
+ must be ignored; Question2 has comments on only the first and third of three
6
+ answers, covering index alignment with scoring_data_structs.
7
+ -->
8
+ <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">
9
+ <assessment ident="i98661073a66c6b2567c8f9b1a22c2989" title="Numeric feedback">
10
+ <section ident="root_section">
11
+ <item ident="ia63b6a115fa68a555398a58626a62f82" title="Question1">
12
+ <presentation>
13
+ <material>
14
+ <mattext texttype="text/html">&lt;div&gt;&lt;p&gt;question 1&lt;/p&gt;&lt;/div&gt;</mattext>
15
+ </material>
16
+ <response_str ident="response1" rcardinality="Single">
17
+ <render_fib fibtype="Decimal">
18
+ <response_label ident="answer1"/>
19
+ </render_fib>
20
+ </response_str>
21
+ </presentation>
22
+ <resprocessing>
23
+ <outcomes>
24
+ <decvar maxvalue="100" minvalue="0" varname="SCORE" vartype="Decimal"/>
25
+ </outcomes>
26
+ <respcondition continue="No">
27
+ <conditionvar>
28
+ <or>
29
+ <varequal respident="response1">42.0</varequal>
30
+ <and>
31
+ <vargte respident="response1">42.0</vargte>
32
+ <varlte respident="response1">42.0</varlte>
33
+ </and>
34
+ </or>
35
+ </conditionvar>
36
+ <setvar action="Set" varname="SCORE">100</setvar>
37
+ <displayfeedback feedbacktype="Response" linkrefid="1001_fb"/>
38
+ <displayfeedback feedbacktype="Response" linkrefid="correct_fb"/>
39
+ </respcondition>
40
+ <respcondition continue="No">
41
+ <conditionvar>
42
+ <or>
43
+ <varequal respident="response1">1000.00</varequal>
44
+ <and>
45
+ <vargt respident="response1">999.995</vargt>
46
+ <varlte respident="response1">1000.005</varlte>
47
+ </and>
48
+ </or>
49
+ </conditionvar>
50
+ <setvar action="Set" varname="SCORE">100</setvar>
51
+ <displayfeedback feedbacktype="Response" linkrefid="1002_fb"/>
52
+ </respcondition>
53
+ <respcondition continue="No">
54
+ <conditionvar>
55
+ <or>
56
+ <varequal respident="response1">77.0</varequal>
57
+ <and>
58
+ <vargte respident="response1">70.0</vargte>
59
+ <varlte respident="response1">84.0</varlte>
60
+ </and>
61
+ </or>
62
+ </conditionvar>
63
+ <setvar action="Set" varname="SCORE">100</setvar>
64
+ <displayfeedback feedbacktype="Response" linkrefid="1003_fb"/>
65
+ </respcondition>
66
+ <respcondition continue="No">
67
+ <conditionvar>
68
+ <vargte respident="response1">3.9</vargte>
69
+ <varlte respident="response1">4.1</varlte>
70
+ </conditionvar>
71
+ <setvar action="Set" varname="SCORE">100</setvar>
72
+ <displayfeedback feedbacktype="Response" linkrefid="1004_fb"/>
73
+ </respcondition>
74
+ </resprocessing>
75
+ <itemfeedback ident="1001_fb">
76
+ <flow_mat>
77
+ <material>
78
+ <mattext texttype="text/html">&lt;p&gt;Exact answer comment&lt;/p&gt;</mattext>
79
+ </material>
80
+ </flow_mat>
81
+ </itemfeedback>
82
+ <itemfeedback ident="1002_fb">
83
+ <flow_mat>
84
+ <material>
85
+ <mattext texttype="text/html">&lt;p&gt;Precision answer comment&lt;/p&gt;</mattext>
86
+ </material>
87
+ </flow_mat>
88
+ </itemfeedback>
89
+ <itemfeedback ident="1003_fb">
90
+ <flow_mat>
91
+ <material>
92
+ <mattext texttype="text/html">&lt;p&gt;Margin of error answer comment&lt;/p&gt;</mattext>
93
+ </material>
94
+ </flow_mat>
95
+ </itemfeedback>
96
+ <itemfeedback ident="1004_fb">
97
+ <flow_mat>
98
+ <material>
99
+ <mattext texttype="text/html">&lt;p&gt;Range answer comment&lt;/p&gt;</mattext>
100
+ </material>
101
+ </flow_mat>
102
+ </itemfeedback>
103
+ <itemfeedback ident="correct_fb">
104
+ <flow_mat>
105
+ <material>
106
+ <mattext texttype="text/html">&lt;p&gt;General Correct Feedback&lt;/p&gt;</mattext>
107
+ </material>
108
+ </flow_mat>
109
+ </itemfeedback>
110
+ <itemfeedback ident="general_fb">
111
+ <flow_mat>
112
+ <material>
113
+ <mattext texttype="text/html">&lt;p&gt;General Feedback&lt;/p&gt;</mattext>
114
+ </material>
115
+ </flow_mat>
116
+ </itemfeedback>
117
+ </item>
118
+ <item ident="ia63b6a115fa68a555398a58626a62f83" title="Question2">
119
+ <presentation>
120
+ <material>
121
+ <mattext texttype="text/html">&lt;div&gt;&lt;p&gt;question 2&lt;/p&gt;&lt;/div&gt;</mattext>
122
+ </material>
123
+ <response_str ident="response1" rcardinality="Single">
124
+ <render_fib fibtype="Decimal">
125
+ <response_label ident="answer1"/>
126
+ </render_fib>
127
+ </response_str>
128
+ </presentation>
129
+ <resprocessing>
130
+ <outcomes>
131
+ <decvar maxvalue="100" minvalue="0" varname="SCORE" vartype="Decimal"/>
132
+ </outcomes>
133
+ <respcondition continue="No">
134
+ <conditionvar>
135
+ <or>
136
+ <varequal respident="response1">10.0</varequal>
137
+ <and>
138
+ <vargte respident="response1">10.0</vargte>
139
+ <varlte respident="response1">10.0</varlte>
140
+ </and>
141
+ </or>
142
+ </conditionvar>
143
+ <setvar action="Set" varname="SCORE">100</setvar>
144
+ <displayfeedback feedbacktype="Response" linkrefid="2001_fb"/>
145
+ </respcondition>
146
+ <respcondition continue="No">
147
+ <conditionvar>
148
+ <or>
149
+ <varequal respident="response1">20.0</varequal>
150
+ <and>
151
+ <vargte respident="response1">20.0</vargte>
152
+ <varlte respident="response1">20.0</varlte>
153
+ </and>
154
+ </or>
155
+ </conditionvar>
156
+ <setvar action="Set" varname="SCORE">100</setvar>
157
+ </respcondition>
158
+ <respcondition continue="No">
159
+ <conditionvar>
160
+ <or>
161
+ <varequal respident="response1">30.0</varequal>
162
+ <and>
163
+ <vargte respident="response1">30.0</vargte>
164
+ <varlte respident="response1">30.0</varlte>
165
+ </and>
166
+ </or>
167
+ </conditionvar>
168
+ <setvar action="Set" varname="SCORE">100</setvar>
169
+ <displayfeedback feedbacktype="Response" linkrefid="2003_fb"/>
170
+ </respcondition>
171
+ </resprocessing>
172
+ <itemfeedback ident="2001_fb">
173
+ <flow_mat>
174
+ <material>
175
+ <mattext texttype="text/html">&lt;p&gt;First answer comment&lt;/p&gt;</mattext>
176
+ </material>
177
+ </flow_mat>
178
+ </itemfeedback>
179
+ <itemfeedback ident="2003_fb">
180
+ <flow_mat>
181
+ <material>
182
+ <mattext texttype="text/html">&lt;p&gt;Third answer comment&lt;/p&gt;</mattext>
183
+ </material>
184
+ </flow_mat>
185
+ </itemfeedback>
186
+ </item>
187
+ </section>
188
+ </assessment>
189
+ </questestinterop>
@@ -140,4 +140,62 @@ describe Qti::V1::Models::Interactions::NumericInteraction do
140
140
  end
141
141
  end
142
142
  end
143
+
144
+ context 'answer feedback' do
145
+ let(:xml_file_name) { 'numeric_answer_feedback.xml' }
146
+
147
+ context 'a question with a comment on every answer type' do
148
+ let(:loaded_class) { described_class.new(assessment_item_refs.first, test_object) }
149
+
150
+ it 'returns a comment for the exact, precision, margin of error and range answers' do
151
+ expect(loaded_class.answer_feedback).to eq(
152
+ [
153
+ { response_index: 0, texttype: 'text/html', feedback: '<p>Exact answer comment</p>' },
154
+ { response_index: 1, texttype: 'text/html', feedback: '<p>Precision answer comment</p>' },
155
+ { response_index: 2, texttype: 'text/html', feedback: '<p>Margin of error answer comment</p>' },
156
+ { response_index: 3, texttype: 'text/html', feedback: '<p>Range answer comment</p>' }
157
+ ]
158
+ )
159
+ end
160
+
161
+ it 'aligns every response_index with the matching scoring_data_struct' do
162
+ structs = loaded_class.scoring_data_structs
163
+ expect(structs.map(&:type)).to eq(
164
+ %w[exactResponse preciseResponse marginOfError withinARange]
165
+ )
166
+ expect(loaded_class.answer_feedback.map { |fb| structs[fb[:response_index]].type }).to eq(
167
+ %w[exactResponse preciseResponse marginOfError withinARange]
168
+ )
169
+ end
170
+
171
+ it 'ignores the item level feedback' do
172
+ expect(loaded_class.answer_feedback.map { |fb| fb[:feedback] }).not_to include(
173
+ '<p>General Correct Feedback</p>', '<p>General Feedback</p>'
174
+ )
175
+ end
176
+ end
177
+
178
+ context 'a question where only some answers have a comment' do
179
+ let(:loaded_class) { described_class.new(assessment_item_refs[1], test_object) }
180
+
181
+ it 'keeps the index of the commented answers aligned with scoring_data_structs' do
182
+ expect(loaded_class.answer_feedback).to eq(
183
+ [
184
+ { response_index: 0, texttype: 'text/html', feedback: '<p>First answer comment</p>' },
185
+ { response_index: 2, texttype: 'text/html', feedback: '<p>Third answer comment</p>' }
186
+ ]
187
+ )
188
+ expect(loaded_class.scoring_data_structs.map(&:value)).to eq(%w[10.0 20.0 30.0])
189
+ end
190
+ end
191
+ end
192
+
193
+ context 'without answer feedback' do
194
+ let(:xml_file_name) { 'numeric_exact_match.xml' }
195
+ let(:loaded_class) { described_class.new(assessment_item_refs[1], test_object) }
196
+
197
+ it 'returns nil' do
198
+ expect(loaded_class.answer_feedback).to be_nil
199
+ end
200
+ end
143
201
  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.29.0
4
+ version: 2.30.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: 2026-07-24 00:00:00.000000000 Z
15
+ date: 2026-09-07 00:00:00.000000000 Z
16
16
  dependencies:
17
17
  - !ruby/object:Gem::Dependency
18
18
  name: actionview
@@ -412,6 +412,7 @@ files:
412
412
  - spec/fixtures/items_1.2/nq_multiple_fib_answer_types.xml
413
413
  - spec/fixtures/items_1.2/nq_multiple_fib_case_sensitive.xml
414
414
  - spec/fixtures/items_1.2/nq_multiple_fib_scoring_algorithms.xml
415
+ - spec/fixtures/items_1.2/numeric_answer_feedback.xml
415
416
  - spec/fixtures/items_1.2/numeric_exact_match.xml
416
417
  - spec/fixtures/items_1.2/numeric_margin_error.xml
417
418
  - spec/fixtures/items_1.2/numeric_margin_error_percent.xml
@@ -797,6 +798,7 @@ test_files:
797
798
  - spec/fixtures/items_1.2/nq_multiple_fib_answer_types.xml
798
799
  - spec/fixtures/items_1.2/nq_multiple_fib_case_sensitive.xml
799
800
  - spec/fixtures/items_1.2/nq_multiple_fib_scoring_algorithms.xml
801
+ - spec/fixtures/items_1.2/numeric_answer_feedback.xml
800
802
  - spec/fixtures/items_1.2/numeric_exact_match.xml
801
803
  - spec/fixtures/items_1.2/numeric_margin_error.xml
802
804
  - spec/fixtures/items_1.2/numeric_margin_error_percent.xml