qti 2.10.0 → 2.12.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4ae3526adbf70df2b7523f98526a3b9f7afd575ec1787d77cd434857b1e017a9
4
- data.tar.gz: 76a12f92afbeda2ab3da260bc6cb8d5633ea731d38659c924662177fdb81e771
3
+ metadata.gz: 36f18933b8d36d5a0c9ff5a5b4788852a9061c434385c3dd542b9cfb6c736c8d
4
+ data.tar.gz: 97641eb71ae7efa662ed2e6052e7cb4f35297e8927a3c01c808dc85dcacbf6e3
5
5
  SHA512:
6
- metadata.gz: a95ea76601e38f46c327770bea336a02c5762bee4ea88afc41587aea2cfd640de4f9aecdf5f404a724d97f49758a43fc35f8fe281abcf151a3c412581f8d00ac
7
- data.tar.gz: f5be814d3d040ce676fda735fd8f9816f563b89526ac8750ba02061477d07bee26f98b59198290feb569caf391502f1b4ffa51da73e44b79ce17f042ccec7fd9
6
+ metadata.gz: db6f0e6c2987afa8da40dd633ae1ecbb3fd5e099ddf6de86d6bf3b65d075caf8cfd4e1a4070810fdca2452ee669f347c33dfbb3d508f8ab44b26ccbc651f2009
7
+ data.tar.gz: bb12cf21f40ea16f3238d7a04f73b8a203b4a54881fd1f1576841abbf26045501e745b358ad77b94d8b4ee1f0ff16fe88876f7d7df073a85efa92ada6e14c75d
data/lib/qti/sanitizer.rb CHANGED
@@ -9,26 +9,37 @@ module Qti
9
9
  }.freeze
10
10
 
11
11
  PROTOCOLS = ['http', 'https', :relative].freeze
12
- FILTER_TAGS = %w[iframe object embed].freeze
12
+ FILTER_TAGS = %w[iframe object embed video audio source].freeze
13
+ MEDIA_SRC_ATTR = %w[src data type codebase].freeze
14
+ MEDIA_FMT_ATTR = %w[width height classid].freeze
15
+ MEDIA_ALT_ATTR = %w[title alt allow allowfullscreen].freeze
16
+ MEDIA_EXT_ATTR = %w[data-media-type data-media-id].freeze
17
+ MEDIA_ATTR = [MEDIA_SRC_ATTR, MEDIA_FMT_ATTR, MEDIA_ALT_ATTR, MEDIA_EXT_ATTR].flatten.freeze
13
18
 
14
19
  CONFIG =
15
20
  {
16
- elements: FILTER_TAGS,
21
+ elements: Sanitize::Config::RELAXED[:elements] + FILTER_TAGS,
17
22
  protocols:
18
23
  {
19
24
  'iframe' => { 'src' => PROTOCOLS },
20
25
  'object' => { 'src' => PROTOCOLS, 'data' => PROTOCOLS },
21
- 'embed' => { 'src' => PROTOCOLS }
22
- }.freeze,
26
+ 'embed' => { 'src' => PROTOCOLS },
27
+ 'video' => { 'src' => PROTOCOLS },
28
+ 'audio' => { 'src' => PROTOCOLS },
29
+ 'source' => { 'src' => PROTOCOLS }
30
+ },
23
31
  attributes:
24
32
  {
25
- 'object' => %w[src width height style data type classid codebase],
33
+ 'video' => MEDIA_ATTR,
34
+ 'audio' => MEDIA_ATTR,
35
+ 'source' => MEDIA_ATTR,
36
+ 'object' => MEDIA_ATTR,
26
37
  'embed' => %w[name src type allowfullscreen pluginspage wmode
27
38
  allowscriptaccess width height],
28
39
  'iframe' => %w[src width height name align frameborder scrolling sandbox
29
40
  allowfullscreen webkitallowfullscreen mozallowfullscreen
30
41
  allow] # TODO: remove explicit allow with domain whitelist account setting
31
- }.freeze
42
+ }
32
43
  }.freeze
33
44
 
34
45
  def clean(html)
@@ -86,7 +97,10 @@ module Qti
86
97
  transformers << object_tag_transformer if import_objects
87
98
  transformers << remap_unknown_tags_transformer
88
99
  transformers << method(:convert_canvas_math_images) if Qti.configuration.extract_latex_from_image_tags
89
- Sanitize::Config::RELAXED.merge transformers: transformers
100
+ Sanitize::Config.merge(
101
+ Sanitize::Config.merge(Sanitize::Config::RELAXED, CONFIG),
102
+ transformers: transformers
103
+ )
90
104
  end
91
105
 
92
106
  def remap_href_path(href)
@@ -10,7 +10,7 @@ module Qti
10
10
  # ensure a prompt is carried into the html
11
11
  prompt = node.at_xpath('//xmlns:prompt')
12
12
  filter_item_body(node)
13
- node.add_child(prompt) if prompt&.parent && prompt.parent != node
13
+ node.add_child(prompt&.dup) if prompt&.parent && prompt.parent != node
14
14
  sanitize_content!(node.to_html)
15
15
  end
16
16
  end
@@ -59,9 +59,11 @@ module Qti
59
59
  def stem_text
60
60
  clean_stem_items.search('p').children.map do |stem_item|
61
61
  if stem_item.name == 'gap'
62
+ blank_id = stem_item.attributes['identifier'].value
62
63
  {
63
64
  type: 'blank',
64
- blank_id: stem_item.attributes['identifier'].value
65
+ blank_id: blank_id,
66
+ blank_name: correct_choice_value(blank_id)
65
67
  }
66
68
  else
67
69
  {
@@ -84,12 +86,15 @@ module Qti
84
86
  end
85
87
  end
86
88
 
89
+ def correct_choice_value(node_id)
90
+ answer_choice(choices, question_response_id_mapping[node_id]).content
91
+ end
92
+
87
93
  def scoring_data_structs
88
- mapping = question_response_id_mapping
89
94
  answer_nodes.map do |value_node|
90
95
  node_id = value_node.attributes['identifier']&.value
91
96
  ScoringData.new(
92
- answer_choice(choices, mapping[node_id]).content,
97
+ correct_choice_value(node_id),
93
98
  'directedPair',
94
99
  id: node_id,
95
100
  case: false
@@ -114,11 +119,13 @@ module Qti
114
119
  end
115
120
 
116
121
  def question_response_id_mapping
117
- question_response_pairs = node.xpath('.//xmlns:correctResponse//xmlns:value').map do |value|
118
- value.content.split
122
+ @question_response_id_mapping ||= begin
123
+ question_response_pairs = node.xpath('.//xmlns:correctResponse//xmlns:value').map do |value|
124
+ value.content.split
125
+ end
126
+ question_response_pairs.map!(&:reverse)
127
+ Hash[question_response_pairs]
119
128
  end
120
- question_response_pairs.map!(&:reverse)
121
- Hash[question_response_pairs]
122
129
  end
123
130
  end
124
131
  end
@@ -29,7 +29,7 @@ module Qti
29
29
  def title
30
30
  @title ||= begin
31
31
  QTIV2_TITLE_PATHS.map do |path|
32
- xpath_with_single_check(path)&.content
32
+ @doc.xpath(path).first&.content
33
33
  end.compact.first
34
34
  end || super
35
35
  end
data/lib/qti/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Qti
2
- VERSION = '2.10.0'.freeze
2
+ VERSION = '2.12.0'.freeze
3
3
  end
@@ -0,0 +1,30 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!-- Thie example adapted from the PET Handbook, copyright University of Cambridge ESOL Examinations -->
3
+ <assessmentItem xmlns="http://www.imsglobal.org/xsd/imsqti_v2p2"
4
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5
+ xsi:schemaLocation="http://www.imsglobal.org/xsd/imsqti_v2p2 http://www.imsglobal.org/xsd/qti/qtiv2p2/imsqti_v2p2p2.xsd"
6
+ identifier="choice" title="Unattended Luggage" adaptive="false" timeDependent="false">
7
+ <responseDeclaration identifier="RESPONSE" cardinality="single" baseType="identifier">
8
+ <correctResponse>
9
+ <value>ChoiceA</value>
10
+ </correctResponse>
11
+ </responseDeclaration>
12
+ <outcomeDeclaration identifier="SCORE" cardinality="single" baseType="float">
13
+ <defaultValue>
14
+ <value>0</value>
15
+ </defaultValue>
16
+ </outcomeDeclaration>
17
+ <itemBody>
18
+ <p>Look at the text in the picture.</p>
19
+ <p>
20
+ <img src="images/sign.png" alt="NEVER LEAVE LUGGAGE UNATTENDED"/>
21
+ </p>
22
+ <choiceInteraction responseIdentifier="RESPONSE" shuffle="false" maxChoices="1">
23
+ <prompt>What does it say?</prompt>
24
+ <simpleChoice identifier="ChoiceA">You must stay with your luggage at all times.</simpleChoice>
25
+ <simpleChoice identifier="ChoiceB">Do not let someone else look after your luggage.</simpleChoice>
26
+ <simpleChoice identifier="ChoiceC">Remember your luggage when you leave.</simpleChoice>
27
+ </choiceInteraction>
28
+ </itemBody>
29
+ <responseProcessing template="http://www.imsglobal.org/question/qti_v2p2/rptemplates/match_correct"/>
30
+ </assessmentItem>
@@ -0,0 +1,101 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!--This is a Reload version 1.3 Content Package document-->
3
+ <!--Spawned from the Reload Content Package Generator - http://www.reload.ac.uk-->
4
+ <manifest xmlns="http://www.imsglobal.org/xsd/imscp_v1p1"
5
+ xmlns:imsmd="http://ltsc.ieee.org/xsd/LOM"
6
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
7
+ xmlns:imsqti="http://www.imsglobal.org/xsd/imsqti_metadata_v2p2"
8
+ identifier="MANIFEST-85D76736-6D19-9DC0-7C0B-57C31A9FD390"
9
+ xsi:schemaLocation="http://www.imsglobal.org/xsd/imscp_v1p1 http://www.imsglobal.org/xsd/qti/qtiv2p2/qtiv2p2_imscpv1p2_v1p0.xsd
10
+ http://ltsc.ieee.org/xsd/LOM http://www.imsglobal.org/xsd/imsmd_loose_v1p3p2.xsd
11
+ http://www.imsglobal.org/xsd/imsqti_metadata_v2p2 http://www.imsglobal.org/xsd/qti/qtiv2p2/imsqti_metadata_v2p2.xsd">
12
+ <metadata>
13
+ <schema>QTIv2.2 Package</schema>
14
+ <schemaversion>1.0.0</schemaversion>
15
+ <imsmd:lom>
16
+ <imsmd:general>
17
+ <imsmd:title>
18
+ <imsmd:string>Example Package</imsmd:string>
19
+ </imsmd:title>
20
+ <imsmd:language>en</imsmd:language>
21
+ <imsmd:description>
22
+ <imsmd:string>This is an example Contentpackage containing a
23
+ single QTI v2.2 item</imsmd:string>
24
+ </imsmd:description>
25
+ </imsmd:general>
26
+ <imsmd:lifeCycle>
27
+ <imsmd:version>
28
+ <imsmd:string>2.1</imsmd:string>
29
+ </imsmd:version>
30
+ <imsmd:status>
31
+ <imsmd:source>LOMv1.0</imsmd:source>
32
+ <imsmd:value>Final</imsmd:value>
33
+ </imsmd:status>
34
+ </imsmd:lifeCycle>
35
+ <imsmd:metaMetadata>
36
+ <imsmd:metadataschema>LOMv1.0</imsmd:metadataschema>
37
+ <imsmd:metadataschema>QTIv2.1</imsmd:metadataschema>
38
+ <imsmd:language>en</imsmd:language>
39
+ </imsmd:metaMetadata>
40
+ <imsmd:technical>
41
+ <imsmd:format>text/x-imsqti-item-xml</imsmd:format>
42
+ <imsmd:format>image/png</imsmd:format>
43
+ </imsmd:technical>
44
+ <imsmd:rights>
45
+ <imsmd:description>
46
+ <imsmd:string>(c) 2005, IMS Global Learning Consortium;
47
+ individual questions may have their own copyright statements.</imsmd:string>
48
+ </imsmd:description>
49
+ </imsmd:rights>
50
+ </imsmd:lom>
51
+ </metadata>
52
+ <organizations/>
53
+ <resources>
54
+ <resource identifier="RES-B38DF83F-A291-86DA-4EC3-B2CEBD1515A4" type="imsqti_item_xmlv2p2"
55
+ href="choice.xml">
56
+ <metadata>
57
+ <imsqti:qtiMetadata>
58
+ <imsqti:timeDependent>false</imsqti:timeDependent>
59
+ <imsqti:interactionType>choiceInteraction</imsqti:interactionType>
60
+ <imsqti:feedbackType>none</imsqti:feedbackType>
61
+ <imsqti:solutionAvailable>true</imsqti:solutionAvailable>
62
+ </imsqti:qtiMetadata>
63
+ <imsmd:lom>
64
+ <imsmd:general>
65
+ <imsmd:identifier>
66
+ <imsmd:entry>choice</imsmd:entry>
67
+ </imsmd:identifier>
68
+ <imsmd:title>
69
+ <imsmd:string>Unattended Luggage</imsmd:string>
70
+ </imsmd:title>
71
+ <imsmd:description>
72
+ <imsmd:string>This example illustrates the
73
+ choiceInteraction being used to obtain a single response from the candidate.</imsmd:string>
74
+ </imsmd:description>
75
+ </imsmd:general>
76
+ <imsmd:lifeCycle>
77
+ <imsmd:version>
78
+ <imsmd:string>2.1</imsmd:string>
79
+ </imsmd:version>
80
+ <imsmd:status>
81
+ <imsmd:source>LOMv1.0</imsmd:source>
82
+ <imsmd:value>Final</imsmd:value>
83
+ </imsmd:status>
84
+ </imsmd:lifeCycle>
85
+ <imsmd:technical>
86
+ <imsmd:format>text/x-imsqti-item-xml</imsmd:format>
87
+ <imsmd:format>image/png</imsmd:format>
88
+ </imsmd:technical>
89
+ <imsmd:rights>
90
+ <imsmd:description>
91
+ <imsmd:string>This example has been adapted from the
92
+ PET Handbook, copyright University of Cambridge ESOL Examinations .</imsmd:string>
93
+ </imsmd:description>
94
+ </imsmd:rights>
95
+ </imsmd:lom>
96
+ </metadata>
97
+ <file href="choice.xml"/>
98
+ <file href="images/sign.png"/>
99
+ </resource>
100
+ </resources>
101
+ </manifest>
@@ -0,0 +1,50 @@
1
+ describe Qti::Sanitizer do
2
+ let(:sanitizer) { Qti::Sanitizer.new }
3
+
4
+ describe '#sanitize' do
5
+ it 'keeps media tags' do
6
+ expect(sanitizer.clean('<img>')).to eq('<img>')
7
+ expect(sanitizer.clean('<video>')).to eq('<video></video>')
8
+ expect(sanitizer.clean('<audio>')).to eq('<audio></audio>')
9
+ expect(sanitizer.clean('<object>')).to eq('<object></object>')
10
+ expect(sanitizer.clean('<embed>')).to eq('<embed>')
11
+ end
12
+
13
+ it 'blocks undesirable tags do' do
14
+ expect(sanitizer.clean('<danger>')).to eq('')
15
+ end
16
+
17
+ it 'allows needed media src attributes' do
18
+ html = '<audio src="http://a.url" data="B64" type="media" codebase="???">'
19
+
20
+ expect(sanitizer.clean(html)).to include 'src'
21
+ expect(sanitizer.clean(html)).to include 'data'
22
+ expect(sanitizer.clean(html)).to include 'type'
23
+ expect(sanitizer.clean(html)).to include 'codebase'
24
+ end
25
+
26
+ it 'allows needed media format attributes' do
27
+ html = '<video width="12" height=14 classid="yes">'
28
+
29
+ expect(sanitizer.clean(html)).to include 'width'
30
+ expect(sanitizer.clean(html)).to include 'height'
31
+ expect(sanitizer.clean(html)).to include 'classid'
32
+ end
33
+
34
+ it 'allows needed media extension attributes' do
35
+ html = '<object data-media-type="thing" data-media-id=123456789>'
36
+
37
+ expect(sanitizer.clean(html)).to include 'data-media-type'
38
+ expect(sanitizer.clean(html)).to include 'data-media-id'
39
+ end
40
+
41
+ it 'allows needed media alt attributes' do
42
+ html = '<source title="Title" alt="description" allow="fullscreen" allowfullscreen=1>'
43
+
44
+ expect(sanitizer.clean(html)).to include 'title'
45
+ expect(sanitizer.clean(html)).to include 'alt'
46
+ expect(sanitizer.clean(html)).to include 'allow'
47
+ expect(sanitizer.clean(html)).to include 'allowfullscreen'
48
+ end
49
+ end
50
+ end
@@ -20,6 +20,10 @@ describe Qti::V2::Models::AssessmentItem do
20
20
  expect(loaded_class.item_body).to include 'Look at the text in the picture.'
21
21
  end
22
22
 
23
+ it 'includes the prompt in the item_body' do
24
+ expect(loaded_class.item_body).to include 'What does it say?'
25
+ end
26
+
23
27
  it 'falls back onto nil points possible value' do
24
28
  expect(loaded_class.points_possible).to eq nil
25
29
  end
@@ -43,6 +47,19 @@ describe Qti::V2::Models::AssessmentItem do
43
47
  end
44
48
  end
45
49
 
50
+ context 'gap_match.xml' do
51
+ let(:fixtures_path) { File.join('spec', 'fixtures') }
52
+ let(:file_path) { File.join(fixtures_path, 'items_2.1', 'gap_match.xml') }
53
+ let(:loaded_class) { described_class.from_path!(file_path) }
54
+
55
+ it 'returns the prompt as the first stem item even after calculating item_body' do
56
+ loaded_class.item_body
57
+ stem_items = loaded_class.interaction_model.stem_items
58
+ expect(stem_items.count).to equal(12)
59
+ expect(stem_items.first[:value]).to include('Identify the missing words')
60
+ end
61
+ end
62
+
46
63
  context 'all test files' do
47
64
  test_files = Dir.glob(File.join('spec', 'fixtures', 'items_2.1', '*.xml'))
48
65
  test_files.each do |file|
@@ -11,11 +11,11 @@ describe Qti::V2::Models::Interactions::GapMatchInteraction do
11
11
  value: "Identify the missing words in this famous quote from Shakespeare's Richard III.",
12
12
  id: 'stem_0', position: 1 },
13
13
  { type: 'text', value: 'Now is the ', id: 'stem_1', position: 2 },
14
- { type: 'blank', blank_id: 'G1', id: 'stem_2', position: 3 },
14
+ { type: 'blank', blank_id: 'G1', blank_name: 'winter', id: 'stem_2', position: 3 },
15
15
  { type: 'text', value: ' of our discontent', id: 'stem_3', position: 4 },
16
16
  { type: 'text', value: ' ', id: 'stem_4', position: 5 },
17
17
  { type: 'text', value: ' Made glorious ', id: 'stem_5', position: 6 },
18
- { type: 'blank', blank_id: 'G2', id: 'stem_6', position: 7 },
18
+ { type: 'blank', blank_id: 'G2', blank_name: 'summer', id: 'stem_6', position: 7 },
19
19
  { type: 'text', value: ' by this sun of York;', id: 'stem_7', position: 8 },
20
20
  { type: 'text', value: ' ', id: 'stem_8', position: 9 },
21
21
  { type: 'text', value: " And all the clouds that lour'd\n upon our house",
@@ -33,6 +33,14 @@ describe Qti::V2::Models::NonAssessmentTest do
33
33
  include_examples 'loading_a_non-assessment'
34
34
  end
35
35
 
36
+ describe 'imsqti_2.2_package' do
37
+ let(:path) { File.join(fixtures_path, 'imsqti_2.2_package', 'imsmanifest.xml') }
38
+ let(:loaded_class) { described_class.from_path!(path) }
39
+ let(:title) { 'Example Package' }
40
+
41
+ include_examples 'loading_a_non-assessment'
42
+ end
43
+
36
44
  # describe '#stimulus_ref' do
37
45
  # it 'should return the stimulus ref if it exists' do
38
46
  # item = loaded_class.assessment_items[1]
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.10.0
4
+ version: 2.12.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: 2022-09-09 00:00:00.000000000 Z
15
+ date: 2022-12-14 00:00:00.000000000 Z
16
16
  dependencies:
17
17
  - !ruby/object:Gem::Dependency
18
18
  name: actionview
@@ -54,6 +54,26 @@ dependencies:
54
54
  - - "<"
55
55
  - !ruby/object:Gem::Version
56
56
  version: '6.2'
57
+ - !ruby/object:Gem::Dependency
58
+ name: dry-logic
59
+ requirement: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - "~>"
62
+ - !ruby/object:Gem::Version
63
+ version: 1.2.0
64
+ - - "<"
65
+ - !ruby/object:Gem::Version
66
+ version: '1.3'
67
+ type: :runtime
68
+ prerelease: false
69
+ version_requirements: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - "~>"
72
+ - !ruby/object:Gem::Version
73
+ version: 1.2.0
74
+ - - "<"
75
+ - !ruby/object:Gem::Version
76
+ version: '1.3'
57
77
  - !ruby/object:Gem::Dependency
58
78
  name: dry-struct
59
79
  requirement: !ruby/object:Gem::Requirement
@@ -366,6 +386,9 @@ files:
366
386
  - spec/fixtures/canvas_cartridge/non_cc_assessments/ib5281f3537a06f58877107a2501a4e2d.xml.qti
367
387
  - spec/fixtures/edge_cases_1.2.xml
368
388
  - spec/fixtures/feedback_quiz_1.2.xml
389
+ - spec/fixtures/imsqti_2.2_package/choice.xml
390
+ - spec/fixtures/imsqti_2.2_package/images/sign.png
391
+ - spec/fixtures/imsqti_2.2_package/imsmanifest.xml
369
392
  - spec/fixtures/interaction_checks_1.2.xml
370
393
  - spec/fixtures/items_1.2/canvas_multiple_dropdown.xml
371
394
  - spec/fixtures/items_1.2/canvas_multiple_dropdowns.xml
@@ -639,6 +662,7 @@ files:
639
662
  - spec/lib/qti/models/manifest_spec.rb
640
663
  - spec/lib/qti/models/metadata_spec.rb
641
664
  - spec/lib/qti/models/resource_spec.rb
665
+ - spec/lib/qti/sanitizer_spec.rb
642
666
  - spec/lib/qti/v1/models/assessment_item_spec.rb
643
667
  - spec/lib/qti/v1/models/assessment_spec.rb
644
668
  - spec/lib/qti/v1/models/choices/fill_blank_choice_spec.rb
@@ -719,6 +743,9 @@ test_files:
719
743
  - spec/fixtures/canvas_cartridge/non_cc_assessments/ib5281f3537a06f58877107a2501a4e2d.xml.qti
720
744
  - spec/fixtures/edge_cases_1.2.xml
721
745
  - spec/fixtures/feedback_quiz_1.2.xml
746
+ - spec/fixtures/imsqti_2.2_package/choice.xml
747
+ - spec/fixtures/imsqti_2.2_package/images/sign.png
748
+ - spec/fixtures/imsqti_2.2_package/imsmanifest.xml
722
749
  - spec/fixtures/interaction_checks_1.2.xml
723
750
  - spec/fixtures/items_1.2/canvas_multiple_dropdown.xml
724
751
  - spec/fixtures/items_1.2/canvas_multiple_dropdowns.xml
@@ -992,6 +1019,7 @@ test_files:
992
1019
  - spec/lib/qti/models/manifest_spec.rb
993
1020
  - spec/lib/qti/models/metadata_spec.rb
994
1021
  - spec/lib/qti/models/resource_spec.rb
1022
+ - spec/lib/qti/sanitizer_spec.rb
995
1023
  - spec/lib/qti/v1/models/assessment_item_spec.rb
996
1024
  - spec/lib/qti/v1/models/assessment_spec.rb
997
1025
  - spec/lib/qti/v1/models/choices/fill_blank_choice_spec.rb