qti 0.9.8 → 0.9.9
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/qti.rb +7 -0
- data/lib/qti/v1/models/interactions.rb +0 -1
- data/lib/qti/v1/models/interactions/numeric_interaction.rb +4 -14
- data/lib/qti/v1/models/numerics/exact_match.rb +43 -0
- data/lib/qti/v1/models/numerics/margin_error.rb +51 -0
- data/lib/qti/v1/models/numerics/precision.rb +65 -0
- data/lib/qti/v1/models/numerics/scoring_base.rb +29 -0
- data/lib/qti/v1/models/numerics/scoring_data.rb +26 -0
- data/lib/qti/v1/models/numerics/scoring_node.rb +29 -0
- data/lib/qti/v1/models/numerics/within_range.rb +36 -0
- data/lib/qti/version.rb +1 -1
- data/spec/fixtures/items_1.2/{numeric.xml → numeric_exact_match.xml} +0 -0
- data/spec/fixtures/items_1.2/numeric_margin_error.xml +58 -0
- data/spec/fixtures/items_1.2/numeric_precision.xml +58 -0
- data/spec/fixtures/items_1.2/numeric_within_range.xml +53 -0
- data/spec/gemfiles/nokogiri-1.8.gemfile.lock +35 -18
- data/spec/lib/qti/v1/models/interactions/numeric_interaction_spec.rb +92 -27
- data/spec/lib/qti/v1/models/numerics/exact_match_spec.rb +84 -0
- data/spec/lib/qti/v1/models/numerics/margin_error_spec.rb +80 -0
- data/spec/lib/qti/v1/models/numerics/precision_spec.rb +88 -0
- data/spec/lib/qti/v1/models/numerics/scoring_base_spec.rb +35 -0
- data/spec/lib/qti/v1/models/numerics/scoring_node_spec.rb +29 -0
- data/spec/lib/qti/v1/models/numerics/within_range_spec.rb +64 -0
- data/spec/spec_helper.rb +1 -0
- metadata +44 -17
- data/spec/gemfiles/nokogiri-1.6.gemfile.lock +0 -127
- data/spec/gemfiles/rails-4.2.gemfile.lock +0 -204
- data/spec/gemfiles/rails-5.0.gemfile.lock +0 -210
- data/spec/gemfiles/rails-5.1.gemfile.lock +0 -210
- data/spec/gemfiles/sanitize-4.2.gemfile.lock +0 -127
- data/spec/gemfiles/sanitize-4.5.gemfile.lock +0 -127
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d8fb6165196a5601ea5d6bb415d14897246d6ef7
|
4
|
+
data.tar.gz: a30a49e90c67b24c0762a01c76688520f1633f7b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4dfa4a3312e1a88ec6a37a5282aff21195a624348567ec92c5b9b4798ddfd06deeb6ad67a9b79547299b2b4abe27c602b2d1a74fe48c98af2e65a57802f58b9c
|
7
|
+
data.tar.gz: 3f8f93715a6de83ced239389ff5a58bbbd322f93b1a9a2f53abf72616f922481befa180a08b2b970bae4aea8254b155bace0d16c36b2c34e043d5ae4103d59a0
|
data/lib/qti.rb
CHANGED
@@ -53,6 +53,13 @@ require 'qti/v1/models/choices/logical_identifier_choice'
|
|
53
53
|
require 'qti/v1/models/choices/fill_blank_choice'
|
54
54
|
require 'qti/v1/models/scoring_data'
|
55
55
|
|
56
|
+
require 'qti/v1/models/numerics/exact_match'
|
57
|
+
require 'qti/v1/models/numerics/margin_error'
|
58
|
+
require 'qti/v1/models/numerics/precision'
|
59
|
+
require 'qti/v1/models/numerics/scoring_data'
|
60
|
+
require 'qti/v1/models/numerics/scoring_node'
|
61
|
+
require 'qti/v1/models/numerics/within_range'
|
62
|
+
|
56
63
|
require 'qti/v2/models/base'
|
57
64
|
require 'qti/v2/models/choices/simple_choice'
|
58
65
|
require 'qti/v2/models/choices/simple_associable_choice'
|
@@ -8,7 +8,6 @@ module Qti
|
|
8
8
|
# This one finds the correct parsing model based on the provided xml node
|
9
9
|
def self.interaction_model(node, parent)
|
10
10
|
subclasses = constants.map { |c| const_get(c) }
|
11
|
-
|
12
11
|
# Check for matches
|
13
12
|
matches = subclasses.each_with_object([]) do |interaction_class, result|
|
14
13
|
match = interaction_class.matches(node, parent)
|
@@ -7,8 +7,6 @@ module Qti
|
|
7
7
|
def self.matches(node, parent)
|
8
8
|
first_match = node.at_xpath('.//xmlns:render_fib')
|
9
9
|
return false unless first_match && first_match.attributes['fibtype']&.value == 'Decimal'
|
10
|
-
# exclude types other than "Exact response"
|
11
|
-
return false unless node.at_xpath('.//xmlns:varequal').present?
|
12
10
|
return false if node.xpath('.//xmlns:render_fib').count > 1
|
13
11
|
new(node, parent)
|
14
12
|
end
|
@@ -23,26 +21,18 @@ module Qti
|
|
23
21
|
end
|
24
22
|
end
|
25
23
|
|
26
|
-
def answers
|
27
|
-
@answers ||= answer_nodes.map do |node|
|
28
|
-
V1::Models::Choices::FillBlankChoice.new(node, self)
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
24
|
def scoring_data_structs
|
33
25
|
answer_nodes.map do |value_node|
|
34
|
-
ScoringData.new(
|
35
|
-
value_node
|
36
|
-
|
37
|
-
id: value_node.attributes['respident']&.value
|
38
|
-
)
|
26
|
+
V1::Models::Numerics::ScoringData.new(
|
27
|
+
value_node
|
28
|
+
).scoring_data
|
39
29
|
end
|
40
30
|
end
|
41
31
|
|
42
32
|
private
|
43
33
|
|
44
34
|
def answer_nodes
|
45
|
-
@node.xpath('.//xmlns:
|
35
|
+
@node.xpath('.//xmlns:respcondition')
|
46
36
|
end
|
47
37
|
end
|
48
38
|
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'qti/v1/models/numerics/scoring_base'
|
2
|
+
|
3
|
+
module Qti
|
4
|
+
module V1
|
5
|
+
module Models
|
6
|
+
module Numerics
|
7
|
+
class ExactMatch < ScoringBase
|
8
|
+
def initialize(scoring_node)
|
9
|
+
super(scoring_node)
|
10
|
+
end
|
11
|
+
|
12
|
+
def scoring_data
|
13
|
+
return unless valid?
|
14
|
+
Struct.new(
|
15
|
+
:id,
|
16
|
+
:type,
|
17
|
+
:value
|
18
|
+
).new(
|
19
|
+
equal_node.attributes['respident']&.value,
|
20
|
+
'exactResponse',
|
21
|
+
equal_node.content
|
22
|
+
)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def valid?
|
28
|
+
node_complete? && content_consistent?
|
29
|
+
end
|
30
|
+
|
31
|
+
def node_complete?
|
32
|
+
(equal_node && gte_node && lte_node).present?
|
33
|
+
end
|
34
|
+
|
35
|
+
def content_consistent?
|
36
|
+
equal_node.content == gte_node.content &&
|
37
|
+
equal_node.content == lte_node.content
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'qti/v1/models/numerics/scoring_base'
|
2
|
+
|
3
|
+
module Qti
|
4
|
+
module V1
|
5
|
+
module Models
|
6
|
+
module Numerics
|
7
|
+
class MarginError < ScoringBase
|
8
|
+
def initialize(scoring_node)
|
9
|
+
super(scoring_node)
|
10
|
+
end
|
11
|
+
|
12
|
+
def scoring_data
|
13
|
+
return unless valid?
|
14
|
+
Struct.new(
|
15
|
+
:id,
|
16
|
+
:type,
|
17
|
+
:value,
|
18
|
+
:margin,
|
19
|
+
:margin_type
|
20
|
+
).new(
|
21
|
+
equal_node.attributes['respident']&.value,
|
22
|
+
'marginOfError',
|
23
|
+
equal_node.content.to_f.to_s,
|
24
|
+
margin,
|
25
|
+
'absolute'
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def margin
|
32
|
+
(equal_node.content.to_f - gte_node.content.to_f).to_s
|
33
|
+
end
|
34
|
+
|
35
|
+
def valid?
|
36
|
+
node_complete? && content_consistent?
|
37
|
+
end
|
38
|
+
|
39
|
+
def node_complete?
|
40
|
+
(equal_node && gte_node && lte_node).present?
|
41
|
+
end
|
42
|
+
|
43
|
+
def content_consistent?
|
44
|
+
equal_node.content != gte_node.content &&
|
45
|
+
equal_node.content != lte_node.content
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'qti/v1/models/numerics/scoring_base'
|
2
|
+
|
3
|
+
module Qti
|
4
|
+
module V1
|
5
|
+
module Models
|
6
|
+
module Numerics
|
7
|
+
class Precision < ScoringBase
|
8
|
+
include ActionView::Helpers::NumberHelper
|
9
|
+
|
10
|
+
def initialize(scoring_node)
|
11
|
+
super(scoring_node)
|
12
|
+
end
|
13
|
+
|
14
|
+
def scoring_data
|
15
|
+
return unless equal_node && gt_node && lte_node
|
16
|
+
Struct.new(
|
17
|
+
:id,
|
18
|
+
:type,
|
19
|
+
:value,
|
20
|
+
:precision,
|
21
|
+
:precision_type
|
22
|
+
).new(
|
23
|
+
equal_node.attributes['respident']&.value,
|
24
|
+
'preciseResponse',
|
25
|
+
value,
|
26
|
+
precision.to_s,
|
27
|
+
'significantDigits'
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
def value
|
32
|
+
fval = equal_node.content.to_f
|
33
|
+
number_with_precision(fval, precision: precision, significant: true)
|
34
|
+
end
|
35
|
+
|
36
|
+
def precision
|
37
|
+
sig = [
|
38
|
+
Precision.significant_digits(gt_node.content),
|
39
|
+
Precision.significant_digits(lte_node.content)
|
40
|
+
].max
|
41
|
+
sig - 1
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.significant_digits(number_s)
|
45
|
+
sig = 0
|
46
|
+
zeros = 0
|
47
|
+
number_s.each_char do |c|
|
48
|
+
next if (sig.zero? && c == '0') || c == '.'
|
49
|
+
if c == '0'
|
50
|
+
zeros += 1
|
51
|
+
else
|
52
|
+
sig += zeros + 1
|
53
|
+
zeros = 0
|
54
|
+
end
|
55
|
+
end
|
56
|
+
# this is not exactly significant digits
|
57
|
+
# trailing zeros are ignored
|
58
|
+
# because gt_node and lte_ndoe don't expect trailing zeros
|
59
|
+
sig
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Qti
|
2
|
+
module V1
|
3
|
+
module Models
|
4
|
+
module Numerics
|
5
|
+
class ScoringBase
|
6
|
+
def initialize(scoring_node)
|
7
|
+
@scoring_node = scoring_node
|
8
|
+
end
|
9
|
+
|
10
|
+
def equal_node
|
11
|
+
@scoring_node.equal_node
|
12
|
+
end
|
13
|
+
|
14
|
+
def gte_node
|
15
|
+
@scoring_node.gte_node
|
16
|
+
end
|
17
|
+
|
18
|
+
def lte_node
|
19
|
+
@scoring_node.lte_node
|
20
|
+
end
|
21
|
+
|
22
|
+
def gt_node
|
23
|
+
@scoring_node.gt_node
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Qti
|
2
|
+
module V1
|
3
|
+
module Models
|
4
|
+
module Numerics
|
5
|
+
class ScoringData
|
6
|
+
class UnsupportedNumreicType < StandardError; end
|
7
|
+
def initialize(node)
|
8
|
+
@scoring_node = ScoringNode.new(node)
|
9
|
+
end
|
10
|
+
|
11
|
+
def scoring_data
|
12
|
+
ExactMatch.new(@scoring_node).scoring_data ||
|
13
|
+
MarginError.new(@scoring_node).scoring_data ||
|
14
|
+
Precision.new(@scoring_node).scoring_data ||
|
15
|
+
WithinRange.new(@scoring_node).scoring_data ||
|
16
|
+
unknown_type
|
17
|
+
end
|
18
|
+
|
19
|
+
def unknown_type
|
20
|
+
raise UnsupportedNumreicType, 'Unsupported Numeric Type'
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Qti
|
2
|
+
module V1
|
3
|
+
module Models
|
4
|
+
module Numerics
|
5
|
+
class ScoringNode
|
6
|
+
def initialize(node)
|
7
|
+
@node = node
|
8
|
+
end
|
9
|
+
|
10
|
+
def equal_node
|
11
|
+
@_equal_node ||= @node.at_xpath('.//xmlns:varequal')
|
12
|
+
end
|
13
|
+
|
14
|
+
def gte_node
|
15
|
+
@_gte_node ||= @node.at_xpath('.//xmlns:vargte')
|
16
|
+
end
|
17
|
+
|
18
|
+
def lte_node
|
19
|
+
@_lte_node ||= @node.at_xpath('.//xmlns:varlte')
|
20
|
+
end
|
21
|
+
|
22
|
+
def gt_node
|
23
|
+
@_gt_node ||= @node.at_xpath('.//xmlns:vargt')
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'qti/v1/models/numerics/scoring_base'
|
2
|
+
|
3
|
+
module Qti
|
4
|
+
module V1
|
5
|
+
module Models
|
6
|
+
module Numerics
|
7
|
+
class WithinRange < ScoringBase
|
8
|
+
def initialize(scoring_node)
|
9
|
+
super(scoring_node)
|
10
|
+
end
|
11
|
+
|
12
|
+
def scoring_data
|
13
|
+
return unless valid?
|
14
|
+
Struct.new(
|
15
|
+
:id,
|
16
|
+
:type,
|
17
|
+
:start,
|
18
|
+
:end
|
19
|
+
).new(
|
20
|
+
gte_node.attributes['respident']&.value,
|
21
|
+
'withinARange',
|
22
|
+
gte_node.content,
|
23
|
+
lte_node.content
|
24
|
+
)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def valid?
|
30
|
+
(gte_node && lte_node).present? && equal_node.nil?
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/qti/version.rb
CHANGED
File without changes
|
@@ -0,0 +1,58 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
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
|
+
<assessment ident="i3c06f375b6b24a39925dc6a213421021" title="Quiz1.Margin.Error">
|
4
|
+
<qtimetadata>
|
5
|
+
<qtimetadatafield>
|
6
|
+
<fieldlabel>cc_maxattempts</fieldlabel>
|
7
|
+
<fieldentry>1</fieldentry>
|
8
|
+
</qtimetadatafield>
|
9
|
+
</qtimetadata>
|
10
|
+
<section ident="root_section">
|
11
|
+
<item ident="i028a5c60d8ce9dc63df3afefa2e9d01e" title="Question">
|
12
|
+
<itemmetadata>
|
13
|
+
<qtimetadata>
|
14
|
+
<qtimetadatafield>
|
15
|
+
<fieldlabel>question_type</fieldlabel>
|
16
|
+
<fieldentry>numerical_question</fieldentry>
|
17
|
+
</qtimetadatafield>
|
18
|
+
<qtimetadatafield>
|
19
|
+
<fieldlabel>points_possible</fieldlabel>
|
20
|
+
<fieldentry>1.0</fieldentry>
|
21
|
+
</qtimetadatafield>
|
22
|
+
<qtimetadatafield>
|
23
|
+
<fieldlabel>assessment_question_identifierref</fieldlabel>
|
24
|
+
<fieldentry>i874908082e9dce80647f2771296ca69c</fieldentry>
|
25
|
+
</qtimetadatafield>
|
26
|
+
</qtimetadata>
|
27
|
+
</itemmetadata>
|
28
|
+
<presentation>
|
29
|
+
<material>
|
30
|
+
<mattext texttype="text/html"><div><p>QQ1.margin error</p></div></mattext>
|
31
|
+
</material>
|
32
|
+
<response_str ident="response1" rcardinality="Single">
|
33
|
+
<render_fib fibtype="Decimal">
|
34
|
+
<response_label ident="answer1"/>
|
35
|
+
</render_fib>
|
36
|
+
</response_str>
|
37
|
+
</presentation>
|
38
|
+
<resprocessing>
|
39
|
+
<outcomes>
|
40
|
+
<decvar maxvalue="100" minvalue="0" varname="SCORE" vartype="Decimal"/>
|
41
|
+
</outcomes>
|
42
|
+
<respcondition continue="No">
|
43
|
+
<conditionvar>
|
44
|
+
<or>
|
45
|
+
<varequal respident="response1">77.0</varequal>
|
46
|
+
<and>
|
47
|
+
<vargte respident="response1">70.0</vargte>
|
48
|
+
<varlte respident="response1">84.0</varlte>
|
49
|
+
</and>
|
50
|
+
</or>
|
51
|
+
</conditionvar>
|
52
|
+
<setvar action="Set" varname="SCORE">100</setvar>
|
53
|
+
</respcondition>
|
54
|
+
</resprocessing>
|
55
|
+
</item>
|
56
|
+
</section>
|
57
|
+
</assessment>
|
58
|
+
</questestinterop>
|
@@ -0,0 +1,58 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
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
|
+
<assessment ident="id2b817165c302c0c57416146dc94ce47" title="Quiz1.precision">
|
4
|
+
<qtimetadata>
|
5
|
+
<qtimetadatafield>
|
6
|
+
<fieldlabel>cc_maxattempts</fieldlabel>
|
7
|
+
<fieldentry>1</fieldentry>
|
8
|
+
</qtimetadatafield>
|
9
|
+
</qtimetadata>
|
10
|
+
<section ident="root_section">
|
11
|
+
<item ident="i3ac3c6ccf4dd3dce4d5b5a5721edd5d5" title="Question">
|
12
|
+
<itemmetadata>
|
13
|
+
<qtimetadata>
|
14
|
+
<qtimetadatafield>
|
15
|
+
<fieldlabel>question_type</fieldlabel>
|
16
|
+
<fieldentry>numerical_question</fieldentry>
|
17
|
+
</qtimetadatafield>
|
18
|
+
<qtimetadatafield>
|
19
|
+
<fieldlabel>points_possible</fieldlabel>
|
20
|
+
<fieldentry>1.0</fieldentry>
|
21
|
+
</qtimetadatafield>
|
22
|
+
<qtimetadatafield>
|
23
|
+
<fieldlabel>assessment_question_identifierref</fieldlabel>
|
24
|
+
<fieldentry>i792f29a40533189006a3bd7ffee3c857</fieldentry>
|
25
|
+
</qtimetadatafield>
|
26
|
+
</qtimetadata>
|
27
|
+
</itemmetadata>
|
28
|
+
<presentation>
|
29
|
+
<material>
|
30
|
+
<mattext texttype="text/html"><div><p>QQ1.presision ss</p></div></mattext>
|
31
|
+
</material>
|
32
|
+
<response_str ident="response1" rcardinality="Single">
|
33
|
+
<render_fib fibtype="Decimal">
|
34
|
+
<response_label ident="answer1"/>
|
35
|
+
</render_fib>
|
36
|
+
</response_str>
|
37
|
+
</presentation>
|
38
|
+
<resprocessing>
|
39
|
+
<outcomes>
|
40
|
+
<decvar maxvalue="100" minvalue="0" varname="SCORE" vartype="Decimal"/>
|
41
|
+
</outcomes>
|
42
|
+
<respcondition continue="No">
|
43
|
+
<conditionvar>
|
44
|
+
<or>
|
45
|
+
<varequal respident="response1">1000.0</varequal>
|
46
|
+
<and>
|
47
|
+
<vargt respident="response1">999.995</vargt>
|
48
|
+
<varlte respident="response1">1000.005</varlte>
|
49
|
+
</and>
|
50
|
+
</or>
|
51
|
+
</conditionvar>
|
52
|
+
<setvar action="Set" varname="SCORE">100</setvar>
|
53
|
+
</respcondition>
|
54
|
+
</resprocessing>
|
55
|
+
</item>
|
56
|
+
</section>
|
57
|
+
</assessment>
|
58
|
+
</questestinterop>
|