qti 1.0.3 → 1.0.4

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
  SHA1:
3
- metadata.gz: bca00e3ced1649d9c45a507e11f8834b989f1792
4
- data.tar.gz: 25f6fc89f8e60cae72d06f8e68ef548adae14f62
3
+ metadata.gz: 852302a8eb0d02d23199e435c997d027f720c476
4
+ data.tar.gz: c52b00f23b75fc6b0c0d14ccf1bf3e1309b7016b
5
5
  SHA512:
6
- metadata.gz: 8755835f90b0cad59633b9911c20e9f0c206860c7e439fadd2c745ae62f63b54224138844c8a8c0ae2469bd9e076b5a10af031dcf6484276913e560e80bd3599
7
- data.tar.gz: ecd4c1d067f58aba092288032ea306d87a9afe800919d26fdd0ee1f0ecf78a7f2358b25360b0aa8612bdce666ba9decfc1071a74344b93b74cd0f616dc47213e
6
+ metadata.gz: 0cb5f6a63677f60d18ed90dd6729c061f2a64189168f8ffb7bc8ee93693c938ec9fbfa102c0f1ccda4996c2db97b066d38d01c9aa1c40e4a8001f1f63d6ea114
7
+ data.tar.gz: 436eeb7b20019d9f95b61df809993f4b2293fbd06598dd8e2f70d2bb29177093080a2da5633dc4f395e92b968ead921abb0c7a9cf969de8b924146811e138b2a
data/lib/qti.rb CHANGED
@@ -27,7 +27,7 @@ module Qti
27
27
  def self.manifest(path)
28
28
  mpath = manifest_path(path)
29
29
  package_root = File.dirname(mpath)
30
- manifest = Qti::Models::Manifest.from_path!(mpath, package_root)
30
+ manifest = Qti::Models::Manifest.from_path!(mpath, package_root = package_root)
31
31
  [mpath, package_root, manifest]
32
32
  end
33
33
 
@@ -10,19 +10,21 @@ module Qti
10
10
 
11
11
  module Models
12
12
  class Base
13
- attr_reader :doc, :path, :package_root
13
+ attr_reader :doc, :path, :package_root, :resource
14
14
  attr_accessor :manifest
15
+ delegate :metadata, to: :@resource, allow_nil: true
15
16
 
16
17
  def sanitize_content!(html)
17
18
  sanitizer.clean(html)
18
19
  end
19
20
 
20
- def self.from_path!(path, package_root = nil)
21
- new(path: path, package_root: package_root)
21
+ def self.from_path!(path, package_root = nil, resource = nil)
22
+ new(path: path, package_root: package_root, resource: resource)
22
23
  end
23
24
 
24
- def initialize(path:, package_root: nil, html: false)
25
+ def initialize(path:, package_root: nil, html: false, resource: nil)
25
26
  @path = path
27
+ @resource = resource
26
28
  self.package_root = package_root || File.dirname(path)
27
29
  @doc = html ? parse_html(File.read(path)) : parse_xml(File.read(path))
28
30
  raise ArgumentError unless @doc
@@ -76,6 +78,10 @@ module Qti
76
78
  path
77
79
  end
78
80
 
81
+ def raise_unsupported(message = 'Unsupported QTI version')
82
+ raise Qti::UnsupportedSchema, message
83
+ end
84
+
79
85
  protected
80
86
 
81
87
  def package_root=(package_root)
@@ -3,22 +3,13 @@ require 'qti/models/assessment_meta'
3
3
  require 'qti/v1/models/assessment'
4
4
  require 'qti/v2/models/assessment_test'
5
5
  require 'qti/v2/models/non_assessment_test'
6
+ require 'qti/models/resource'
6
7
 
7
8
  module Qti
8
9
  module Models
9
10
  class Manifest < Qti::Models::Base
10
- RESOURCE_QTI_TYPES = %w[imsqti_test_xmlv2p1
11
- imsqti_test_xmlv2p2
12
- imsqti_xmlv1p2].freeze
13
- ASSESSMENT_CLASSES = {
14
- 'imsqti_xmlv1p2' => Qti::V1::Models::Assessment,
15
- 'imsqti_test_xmlv2p1' => Qti::V2::Models::AssessmentTest,
16
- 'imsqti_test_xmlv2p2' => Qti::V2::Models::AssessmentTest
17
- }.freeze
18
- EMBEDDED_QTI_TYPES = %w[imsqti_item_xmlv2p1
19
- imsqti_item_xmlv2p2].freeze
20
- EMBEDDED_NON_ASSESSMENT_ID = '@embedded_non_assessment'.freeze
21
-
11
+ include Qti::Models::ResourceGroup
12
+ include Qti::XPathHelpers
22
13
  def assessment_test(resource_id = nil)
23
14
  resource_id ||= assessment_identifiers.first
24
15
  test = assessment_from_identifier(resource_id)
@@ -26,31 +17,11 @@ module Qti
26
17
  test
27
18
  end
28
19
 
29
- def raise_unsupported
30
- raise Qti::UnsupportedSchema, 'Unsupported QTI version'
31
- end
32
-
33
- def assessment_identifiers(embedded_as_assessment = true)
34
- id_list = identifier_list('/assessment')
35
- return id_list + [EMBEDDED_NON_ASSESSMENT_ID] if embedded_as_assessment && embedded_non_assessment?
36
- id_list
37
- end
38
-
39
- def question_bank_identifiers
40
- identifier_list('/question-bank')
41
- end
42
-
43
- def identifier_list(rsc_type)
44
- RESOURCE_QTI_TYPES.map do |v|
45
- xmlns_resource_list("[#{rtype_predicate(v, rsc_type)}]").map { |r| r[:identifier] }
46
- end.flatten
47
- end
48
-
49
20
  private
50
21
 
51
22
  def assessment_from_identifier(identifier)
52
23
  return embedded_non_assessment if identifier == EMBEDDED_NON_ASSESSMENT_ID
53
- rsc_ver = xpath_with_single_check(xpath_xmlns_resource("[@identifier='#{identifier}']"))&.[](:type)
24
+ rsc_ver = xpath_with_single_check(xpath_resource("[@identifier='#{identifier}']"))&.[](:type)
54
25
  raise_unsupported unless rsc_ver
55
26
  assessment_from(rsc_ver, identifier)
56
27
  end
@@ -58,73 +29,24 @@ module Qti
58
29
  def assessment_from(version, identifier)
59
30
  builder = ASSESSMENT_CLASSES[version.split('/').first]
60
31
  raise_unsupported unless builder
61
- canvas_meta = canvas_meta_data_for(identifier)
62
- assessment = builder.from_path!(
63
- remap_href_path(asset_resource_for(identifier, version, canvas_meta&.quiz_identifier)),
64
- @package_root
65
- )
66
- assessment.canvas_meta_data(canvas_meta_data_for(identifier))
32
+ rsc = resource_for(identifier, version)
33
+ assessment = builder.from_path!(remap_href_path(asset_resource_for(rsc)), @package_root, rsc)
34
+ assessment.canvas_meta_data(rsc.canvas_metadata)
67
35
  assessment
68
36
  end
69
37
 
70
- def asset_resource_for(identifier, qti_type, canvas_ident)
71
- asset_resource_for_canvas(canvas_ident) || asset_resource_for_ims(identifier, qti_type)
72
- end
73
-
74
- def asset_resource_for_canvas(identifier)
75
- canvas_extra_file(identifier, '.xml.qti')
76
- end
77
-
78
- def asset_resource_for_ims(identifier, qti_type)
79
- base_xpath = "[@identifier='#{identifier}' and starts-with(@type, '#{qti_type}')]"
80
- xmlns_resource(base_xpath + '/@href') || xmlns_resource(base_xpath + '/xmlns:file/@href')
81
- end
82
-
83
- def dependency_id(identifier)
84
- xmlns_resource("[@identifier='#{identifier}']/xmlns:dependency/@identifierref")
85
- end
86
-
87
- def canvas_meta_data_for(identifier)
88
- meta_file = canvas_extra_file(identifier, 'assessment_meta.xml')
89
- return Qti::Models::AssessmentMeta.from_path!(File.join(@package_root, meta_file)) if meta_file
90
- end
91
-
92
- def canvas_extra_file(identifier, filename)
93
- dep_id = dependency_id(identifier)
94
- xmlns_resource(
95
- "[@identifier='#{dep_id}']/xmlns:file[#{xpath_endswith('@href', filename)}]/@href"
96
- )
97
- end
98
-
99
38
  def xmlns_resource(type)
100
- xpath_with_single_check(xpath_xmlns_resource(type))&.content
39
+ xpath_with_single_check(xpath_resource(type))
101
40
  end
102
41
 
103
42
  def xmlns_resource_list(type)
104
- @doc.xpath(xpath_xmlns_resource(type))
43
+ @doc.xpath(xpath_resource(type))
105
44
  end
106
45
 
107
46
  def xmlns_resource_count(type)
108
47
  xmlns_resource_list(type).count
109
48
  end
110
49
 
111
- def xpath_xmlns_resource(type = '')
112
- "//xmlns:resources/xmlns:resource#{type}"
113
- end
114
-
115
- def xpath_endswith(tag, tail)
116
- "substring(#{tag}, string-length(#{tag}) - string-length('#{tail}') + 1) = '#{tail}'"
117
- end
118
-
119
- def rtype_predicate(ver, rsc_type)
120
- # XPath 2.0 supports ends-with, which is what substring is doing here.
121
- # It also support regex matching with matches.
122
- # We only have XPath 1.0 available.
123
- cc_match = "starts-with(@type, '#{ver}') and " + xpath_endswith('@type', rsc_type)
124
- qti_match = "@type='#{ver}'"
125
- "#{qti_match} or (#{cc_match})"
126
- end
127
-
128
50
  def embedded_non_assessment?
129
51
  EMBEDDED_QTI_TYPES.map { |typ| xmlns_resource_count("[@type='#{typ}']/@href") }.flatten.sum.positive?
130
52
  end
@@ -0,0 +1,63 @@
1
+ module Qti
2
+ module Models
3
+ class MetaData < Qti::Models::Base
4
+ def initialize(node)
5
+ @node = node
6
+ end
7
+
8
+ def taxonpaths
9
+ return unless lom
10
+ hier = Hash.new { |h, k| h[k] = [] }
11
+ lom.xpath('imsmd:classification/imsmd:taxonPath').each do |tp|
12
+ entry = taxonpath_entry(tp)
13
+ hier[entry[:source]] = entry[:taxonpath]
14
+ end
15
+ hier
16
+ end
17
+
18
+ private
19
+
20
+ def taxonpath_entry(node)
21
+ {
22
+ source: node.xpath('imsmd:source/imsmd:string').text,
23
+ taxonpath: taxons(node)
24
+ }
25
+ end
26
+
27
+ def metadata
28
+ @metadata ||= @node&.xpath('xmlns:metadata')&.first
29
+ end
30
+
31
+ def lom
32
+ return unless imsmd
33
+ @lom ||= metadata&.xpath('imsmd:lom')&.first
34
+ end
35
+
36
+ def imsmd
37
+ @node.namespaces&.keys&.include?('xmlns:imsmd')
38
+ end
39
+
40
+ def taxons(node)
41
+ hier = Hash.new { |h, k| h[k] = [] }
42
+ taxon(node, hier)
43
+ end
44
+
45
+ def taxon(node, path)
46
+ return path unless node
47
+ xpath = 'imsmd:taxon/imsmd:entry/*[self::imsmd:string or self::imsmd:langstring]'
48
+ node.xpath(xpath).each do |taxon|
49
+ lang = taxon.attr('language') || taxon.attr('lang') || taxon.attr('xml:lang') || 'default'
50
+ path[lang].push(taxon.text)
51
+ end
52
+ taxon(node.xpath('imsmd:taxon')&.first, path)
53
+ end
54
+ end
55
+
56
+ module MetaDataBase
57
+ delegate :taxonpath, to: :@meta_data, allow_nil: true
58
+ def metadata_from_node!(node)
59
+ @meta_data ||= MetaData.new(node)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,115 @@
1
+ require 'qti/v1/models/base'
2
+ require 'qti/models/assessment_meta'
3
+ require 'qti/v1/models/assessment'
4
+ require 'qti/v2/models/assessment_test'
5
+ require 'qti/v2/models/non_assessment_test'
6
+ require 'qti/xpath_helpers'
7
+ require 'qti/models/metadata'
8
+
9
+ module Qti
10
+ module Models
11
+ class Resource < Qti::Models::Base
12
+ include Qti::Models::MetaDataBase
13
+ include Qti::XPathHelpers
14
+ def initialize(node, parent)
15
+ @node = node
16
+ @parent = parent
17
+ @resource_type = node.attr('type')
18
+ @identifier = node.attr('identifier')
19
+ copy_paths_from_item(parent)
20
+ end
21
+
22
+ def href
23
+ @href ||= @node.attr('href') || @node.xpath('xmlns:file/@href')&.first&.content
24
+ end
25
+
26
+ def metadata
27
+ @metadata = metadata_from_node!(@node)
28
+ end
29
+
30
+ def canvas_metadata
31
+ @canvas_meta_file ||= canvas_extra_file('assessment_meta.xml')
32
+ return unless @canvas_meta_file
33
+ meta_file = File.join(@package_root, @canvas_meta_file)
34
+ @canvas_metadata ||= Qti::Models::AssessmentMeta.from_path!(meta_file) if @canvas_meta_file
35
+ end
36
+
37
+ def canvas_extra_file(filename)
38
+ dep_id = dependency_id
39
+ rsc = @parent.resource_node(
40
+ "[@identifier='#{dep_id}']/xmlns:file[#{xpath_endswith('@href', filename)}]/@href"
41
+ )
42
+ rsc&.content
43
+ end
44
+
45
+ private
46
+
47
+ # Canvas Metadata Helpers
48
+ def dependency_id
49
+ @node.xpath('xmlns:dependency/@identifierref')&.first&.content
50
+ end
51
+ end
52
+
53
+ module ResourceGroup
54
+ RESOURCE_QTI_TYPES = %w[imsqti_test_xmlv2p1
55
+ imsqti_test_xmlv2p2
56
+ imsqti_xmlv1p2].freeze
57
+ ASSESSMENT_CLASSES = {
58
+ 'imsqti_xmlv1p2' => Qti::V1::Models::Assessment,
59
+ 'imsqti_test_xmlv2p1' => Qti::V2::Models::AssessmentTest,
60
+ 'imsqti_test_xmlv2p2' => Qti::V2::Models::AssessmentTest
61
+ }.freeze
62
+ EMBEDDED_QTI_TYPES = %w[imsqti_item_xmlv2p1
63
+ imsqti_item_xmlv2p2].freeze
64
+ EMBEDDED_NON_ASSESSMENT_ID = '@embedded_non_assessment'.freeze
65
+
66
+ def resources(type = '')
67
+ @doc.xpath(xpath_resource(type))
68
+ end
69
+
70
+ def asset_resource_for(rsc)
71
+ asset_resource_for_canvas(rsc) || asset_resource_for_ims(rsc)
72
+ end
73
+
74
+ def asset_resource_for_canvas(rsc)
75
+ rsc.canvas_extra_file('.xml.qti')
76
+ end
77
+
78
+ def asset_resource_for_ims(rsc)
79
+ rsc.href
80
+ end
81
+
82
+ def identifier_list(rsc_type)
83
+ RESOURCE_QTI_TYPES.map do |v|
84
+ xmlns_resource_list("[#{rtype_predicate(v, rsc_type)}]").map { |r| r[:identifier] }
85
+ end.flatten
86
+ end
87
+
88
+ def resource_for(identifier, qti_type = nil)
89
+ qti_type = " and starts-with(@type, '#{qti_type}')" if qti_type
90
+ base_xpath = "[@identifier='#{identifier}'#{qti_type}]"
91
+ Resource.new(resource_node(base_xpath), self)
92
+ end
93
+
94
+ def assessment_identifiers(embedded_as_assessment = true)
95
+ id_list = identifier_list('/assessment')
96
+ return id_list + [EMBEDDED_NON_ASSESSMENT_ID] if embedded_as_assessment && embedded_non_assessment?
97
+ id_list
98
+ end
99
+
100
+ def question_bank_identifiers
101
+ identifier_list('/question-bank')
102
+ end
103
+
104
+ def item_resources_v2
105
+ nodes = resources('[@type="imsqti_item_xmlv2p2"]')
106
+ return nodes if nodes.count >= 1
107
+ resources('[@type="imsqti_item_xmlv2p1"]')
108
+ end
109
+
110
+ def resource_node(type)
111
+ xpath_with_single_check(xpath_resource(type))
112
+ end
113
+ end
114
+ end
115
+ end
@@ -18,7 +18,7 @@ module Qti
18
18
 
19
19
  def create_assessment_item(assessment_item)
20
20
  return nil if sub_section?(assessment_item)
21
- item = Qti::V1::Models::AssessmentItem.new(assessment_item, @package_root)
21
+ item = Qti::V1::Models::AssessmentItem.new(assessment_item, @package_root, self)
22
22
  item.manifest = manifest
23
23
  item
24
24
  end
@@ -6,10 +6,13 @@ module Qti
6
6
  module Models
7
7
  class AssessmentItem < Qti::V1::Models::Base
8
8
  attr_reader :doc
9
+ attr_reader :resource
10
+ delegate :metadata, to: :@resource
9
11
 
10
- def initialize(item, package_root = nil)
12
+ def initialize(item, package_root = nil, resource = nil)
11
13
  @doc = item
12
14
  @path = item.document.url
15
+ @resource = resource
13
16
  self.package_root = package_root
14
17
  end
15
18
 
@@ -1,11 +1,13 @@
1
1
  require 'qti/v2/models/base'
2
2
  require 'qti/models/assessment_meta'
3
+ require 'qti/xpath_helpers'
3
4
 
4
5
  module Qti
5
6
  module V2
6
7
  module Models
7
8
  class AssessmentTest < Qti::V2::Models::Base
8
9
  include Qti::Models::AssessmentMetaBase
10
+ include Qti::XPathHelpers
9
11
  def title
10
12
  @title ||= xpath_with_single_check('//xmlns:assessmentTest/@title')&.content || File.basename(@path, '.xml')
11
13
  end
@@ -14,7 +16,7 @@ module Qti
14
16
  # Return the xml files we should be parsing
15
17
  @assessment_item_reference_hrefs ||= begin
16
18
  @doc.xpath('//xmlns:assessmentItemRef/@href').map(&:content).map do |href|
17
- remap_href_path(href)
19
+ { path: remap_href_path(href), resource: self }
18
20
  end
19
21
  end
20
22
  end
@@ -27,8 +29,8 @@ module Qti
27
29
  @assessment_sections ||= test_parts.first.xpath('//xmlns:assessmentSection')
28
30
  end
29
31
 
30
- def create_assessment_item(assessment_item_ref)
31
- item = Qti::V2::Models::AssessmentItem.from_path!(assessment_item_ref, @package_root)
32
+ def create_assessment_item(ref)
33
+ item = Qti::V2::Models::AssessmentItem.from_path!(ref[:path], @package_root, ref[:resource])
32
34
  item.manifest = manifest
33
35
  item
34
36
  end
@@ -1,31 +1,28 @@
1
1
  require 'qti/v2/models/base'
2
+ require 'qti/models/resource'
2
3
 
3
4
  module Qti
4
5
  module V2
5
6
  module Models
6
7
  class NonAssessmentTest < Qti::V2::Models::AssessmentTest
8
+ include Qti::Models::ResourceGroup
7
9
  def assessment_items
8
10
  # Return the xml files we should be parsing
9
- @assessment_item_reference_hrefs ||= begin
10
- hrefs.map do |href|
11
- remap_href_path(href)
11
+ @assessment_item_resources ||= begin
12
+ item_resources_v2.map do |node|
13
+ rsc = Qti::Models::Resource.new(node, self)
14
+ { path: remap_href_path(rsc.href), resource: rsc }
12
15
  end
13
16
  end
14
17
  end
15
18
 
16
19
  def stimulus_ref(assessment_item_ref)
17
- ref = assessment_item_ref.sub(@package_root, '')
20
+ ref = assessment_item_ref[:path].sub(@package_root, '')
18
21
  dependencies = @doc.xpath("//xmlns:resource[@href='#{ref}']/xmlns:dependency/@identifierref")
19
22
  return unless dependencies&.count == 1
20
23
  href = xpath_with_single_check("//xmlns:resource[@identifier='#{dependencies.first}']/@href")
21
24
  remap_href_path(href)
22
25
  end
23
-
24
- def hrefs
25
- nodes = @doc.xpath("//xmlns:resource[@type='imsqti_item_xmlv2p2']/@href")
26
- return nodes if nodes.count >= 1
27
- @doc.xpath("//xmlns:resource[@type='imsqti_item_xmlv2p1']/@href")
28
- end
29
26
  end
30
27
  end
31
28
  end