qti 1.0.3 → 1.0.4

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
  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