qti 2.10.0 → 2.12.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: 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