qti 0.7.8 → 0.7.9
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 +4 -4
- data/lib/qti.rb +11 -5
- data/lib/qti/models/base.rb +22 -13
- data/lib/qti/models/manifest.rb +1 -1
- data/lib/qti/v1/models/assessment_item.rb +3 -1
- data/lib/qti/v1/models/choices/fill_blank_choice.rb +1 -1
- data/lib/qti/v1/models/choices/logical_identifier_choice.rb +1 -1
- data/lib/qti/v1/models/interactions/match_interaction.rb +1 -1
- data/lib/qti/v2/models/assessment_test.rb +1 -1
- data/lib/qti/v2/models/choices/gap_match_choice.rb +1 -1
- data/lib/qti/v2/models/choices/simple_choice.rb +1 -1
- data/spec/lib/qti/models/base_spec.rb +37 -16
- data/spec/lib/qti_spec.rb +21 -0
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bd980a1e89fbf6817329ae29519f4e266e889906
|
4
|
+
data.tar.gz: f4a2d4935a246d9a07987240059f0c2ca9b03e16
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 66cc18a1302da5dab058a48613df70bb52f6c4211a1ce37dde549844e1fe31931ef3228f8ef74a07044202728efe57f96ec537f8de878338ed72e2acdc7c98c7
|
7
|
+
data.tar.gz: 39b9f7df2da0f51188766863bfd3e2589f8cd907459689b1b797e7439af46d112eec5d58d2c9ae9270f450c9bcddb295bccfe1c88baf34935132bf5a8eaaae7b
|
data/lib/qti.rb
CHANGED
@@ -2,10 +2,16 @@ require 'find'
|
|
2
2
|
|
3
3
|
module Qti
|
4
4
|
class Importer
|
5
|
+
attr_reader :package_root
|
6
|
+
|
5
7
|
def initialize(path)
|
6
8
|
Find.find(path) do |subdir|
|
7
|
-
|
9
|
+
if subdir =~ /imsmanifest.xml\z/
|
10
|
+
@path = subdir
|
11
|
+
break
|
12
|
+
end
|
8
13
|
end
|
14
|
+
@package_root = File.dirname(@path)
|
9
15
|
end
|
10
16
|
|
11
17
|
def import_manifest
|
@@ -20,9 +26,9 @@ module Qti
|
|
20
26
|
|
21
27
|
def version_agnostic_test_object(assessment_test_file)
|
22
28
|
if @manifest.qti_1_href
|
23
|
-
Qti::V1::Models::Assessment.from_path!(assessment_test_file)
|
29
|
+
Qti::V1::Models::Assessment.from_path!(assessment_test_file, @package_root)
|
24
30
|
else
|
25
|
-
Qti::V2::Models::AssessmentTest.from_path!(assessment_test_file)
|
31
|
+
Qti::V2::Models::AssessmentTest.from_path!(assessment_test_file, @package_root)
|
26
32
|
end
|
27
33
|
end
|
28
34
|
|
@@ -38,9 +44,9 @@ module Qti
|
|
38
44
|
|
39
45
|
def create_assessment_item(assessment_item_ref)
|
40
46
|
if @manifest.qti_1_href
|
41
|
-
Qti::V1::Models::AssessmentItem.new(assessment_item_ref)
|
47
|
+
Qti::V1::Models::AssessmentItem.new(assessment_item_ref, @package_root)
|
42
48
|
else
|
43
|
-
Qti::V2::Models::AssessmentItem.from_path!(assessment_item_ref)
|
49
|
+
Qti::V2::Models::AssessmentItem.from_path!(assessment_item_ref, @package_root)
|
44
50
|
end
|
45
51
|
end
|
46
52
|
end
|
data/lib/qti/models/base.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'nokogiri'
|
2
2
|
require 'sanitize'
|
3
|
+
require 'pathname'
|
3
4
|
|
4
5
|
module Qti
|
5
6
|
class ParseError < StandardError; end
|
@@ -8,7 +9,7 @@ module Qti
|
|
8
9
|
|
9
10
|
module Models
|
10
11
|
class Base
|
11
|
-
attr_reader :doc
|
12
|
+
attr_reader :doc, :path, :package_root
|
12
13
|
|
13
14
|
ELEMENTS_REMAP = {
|
14
15
|
'prompt' => 'div',
|
@@ -40,12 +41,13 @@ module Qti
|
|
40
41
|
Sanitize::Config::RELAXED.merge transformers: remap_unknown_tags_transformer
|
41
42
|
end
|
42
43
|
|
43
|
-
def self.from_path!(path)
|
44
|
-
new(path: path)
|
44
|
+
def self.from_path!(path, package_root = nil)
|
45
|
+
new(path: path, package_root: package_root)
|
45
46
|
end
|
46
47
|
|
47
|
-
def initialize(path: nil)
|
48
|
+
def initialize(path:, package_root: nil)
|
48
49
|
@path = path
|
50
|
+
set_package_root(package_root || File.dirname(path))
|
49
51
|
@doc = parse_xml(File.read(path))
|
50
52
|
raise ArgumentError unless @doc
|
51
53
|
end
|
@@ -63,19 +65,26 @@ module Qti
|
|
63
65
|
end
|
64
66
|
|
65
67
|
def parse_xml(xml_string)
|
66
|
-
Nokogiri.XML(xml_string, &:noblanks)
|
68
|
+
Nokogiri.XML(xml_string, @path.to_s, &:noblanks)
|
67
69
|
end
|
68
70
|
|
69
|
-
def remap_href_path(href
|
70
|
-
return
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
href
|
71
|
+
def remap_href_path(href)
|
72
|
+
return nil unless href
|
73
|
+
path = File.join(File.dirname(@path), href)
|
74
|
+
if @package_root.nil?
|
75
|
+
raise Qti::ParseError, "Potentially unsafe href '#{href}'" if href.split('/').include?('..')
|
75
76
|
else
|
76
|
-
|
77
|
-
File.exist?(new_path) ? new_path : href
|
77
|
+
raise Qti::ParseError, "Unsafe href '#{href}'" unless Pathname.new(path).cleanpath.to_s.start_with?(@package_root)
|
78
78
|
end
|
79
|
+
path
|
80
|
+
end
|
81
|
+
|
82
|
+
protected
|
83
|
+
|
84
|
+
def set_package_root(package_root)
|
85
|
+
@package_root = package_root
|
86
|
+
return unless @package_root
|
87
|
+
@package_root = Pathname.new(@package_root).cleanpath.to_s + '/'
|
79
88
|
end
|
80
89
|
end
|
81
90
|
end
|
data/lib/qti/models/manifest.rb
CHANGED
@@ -17,7 +17,7 @@ module Qti
|
|
17
17
|
|
18
18
|
def questions
|
19
19
|
node.xpath('.//xmlns:response_lid').map do |lid_node|
|
20
|
-
item_body = lid_node.at_xpath('.//xmlns:mattext').text
|
20
|
+
item_body = sanitize_content!(lid_node.at_xpath('.//xmlns:mattext').text)
|
21
21
|
{ id: lid_node.attributes['ident'].value, itemBody: item_body }
|
22
22
|
end
|
23
23
|
end
|
@@ -45,26 +45,47 @@ describe Qti::Models::Base do
|
|
45
45
|
end
|
46
46
|
|
47
47
|
describe '#remap_href_path' do
|
48
|
-
|
49
|
-
|
50
|
-
let(:remapped_path) { File.join(File.dirname(base_path), href) }
|
51
|
-
let(:subject) { loaded_class.remap_href_path(href, base_path) }
|
52
|
-
|
53
|
-
it 'passes the original path if it exists' do
|
54
|
-
allow(File).to receive(:exist?).with(href).and_return(true)
|
55
|
-
expect(subject).to eq href
|
48
|
+
it 'constructs a path relative to the basename of the source' do
|
49
|
+
expect(loaded_class.remap_href_path('hi.xml')).to eq 'spec/fixtures/test_qti_2.2/hi.xml'
|
56
50
|
end
|
57
51
|
|
58
|
-
it '
|
59
|
-
|
60
|
-
allow(File).to receive(:exist?).with(remapped_path).and_return(false)
|
61
|
-
expect(subject).to eq href
|
52
|
+
it 'is not fooled by an absolute href' do
|
53
|
+
expect(loaded_class.remap_href_path('/etc/shadow')).to eq 'spec/fixtures/test_qti_2.2/etc/shadow'
|
62
54
|
end
|
63
55
|
|
64
|
-
it '
|
65
|
-
|
66
|
-
|
67
|
-
|
56
|
+
it 'uses an implicit package root' do
|
57
|
+
expect {
|
58
|
+
loaded_class.remap_href_path('../sneaky.txt')
|
59
|
+
}.to raise_error(Qti::ParseError)
|
60
|
+
end
|
61
|
+
|
62
|
+
context "with explicit package root" do
|
63
|
+
let(:package_root) { File.join('spec', 'fixtures', 'test_qti_2.2') }
|
64
|
+
let(:item_path) { File.join(package_root, 'true-false', 'true-false.xml') }
|
65
|
+
let(:item) { described_class.from_path!(item_path, package_root) }
|
66
|
+
|
67
|
+
it 'allows safe .. hrefs' do
|
68
|
+
expect {
|
69
|
+
item.remap_href_path('../okay.txt')
|
70
|
+
}.not_to raise_error
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'rejects attempts to escape the package' do
|
74
|
+
expect {
|
75
|
+
item.remap_href_path('../../bad.txt')
|
76
|
+
}.to raise_error(Qti::ParseError)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
context "with nil package root" do
|
81
|
+
let(:item_path) { File.join('spec', 'fixtures', 'test_qti_2.2', 'true-false', 'true-false.xml') }
|
82
|
+
let(:item) { described_class.from_path!(item_path, nil) }
|
83
|
+
|
84
|
+
it 'rejects .. hrefs' do
|
85
|
+
expect {
|
86
|
+
item.remap_href_path('../no_longer_okay.txt')
|
87
|
+
}.to raise_error(Qti::ParseError)
|
88
|
+
end
|
68
89
|
end
|
69
90
|
end
|
70
91
|
end
|
data/spec/lib/qti_spec.rb
CHANGED
@@ -9,6 +9,10 @@ describe Qti::Importer do
|
|
9
9
|
it 'loads an xml file' do
|
10
10
|
expect { importer }.to_not raise_error
|
11
11
|
end
|
12
|
+
|
13
|
+
it 'sets the package root properly' do
|
14
|
+
expect(importer.package_root).to eq file_path
|
15
|
+
end
|
12
16
|
end
|
13
17
|
|
14
18
|
shared_examples_for 'unsupported QTI version' do
|
@@ -33,6 +37,13 @@ describe Qti::Importer do
|
|
33
37
|
answer_arity = assessment_items.map { |item| item.scoring_data_structs.count }
|
34
38
|
expect(answer_arity).to eq [1, 1, 4, 1, 1]
|
35
39
|
end
|
40
|
+
|
41
|
+
it 'sets the path and package root properly' do
|
42
|
+
importer.test_object
|
43
|
+
item = importer.create_assessment_item(importer.assessment_item_refs.first)
|
44
|
+
expect(item.path).to eq file_path + '/quiz.xml'
|
45
|
+
expect(item.package_root).to eq file_path + '/'
|
46
|
+
end
|
36
47
|
end
|
37
48
|
|
38
49
|
context 'canvas generated' do
|
@@ -51,5 +62,15 @@ describe Qti::Importer do
|
|
51
62
|
|
52
63
|
include_examples 'initialize'
|
53
64
|
include_examples 'unsupported QTI version'
|
65
|
+
|
66
|
+
describe '#create_assessment_item' do
|
67
|
+
it 'sets the path and package root properly' do
|
68
|
+
importer.test_object
|
69
|
+
ref = importer.assessment_item_refs.first
|
70
|
+
item = importer.create_assessment_item(ref)
|
71
|
+
expect(item.path).to eq ref
|
72
|
+
expect(item.package_root).to eq file_path + '/'
|
73
|
+
end
|
74
|
+
end
|
54
75
|
end
|
55
76
|
end
|
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: 0.7.
|
4
|
+
version: 0.7.9
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Hannah Bottalla
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2017-08-
|
12
|
+
date: 2017-08-11 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activesupport
|
@@ -488,7 +488,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
488
488
|
version: '0'
|
489
489
|
requirements: []
|
490
490
|
rubyforge_project:
|
491
|
-
rubygems_version: 2.
|
491
|
+
rubygems_version: 2.5.1
|
492
492
|
signing_key:
|
493
493
|
specification_version: 4
|
494
494
|
summary: QTI 1.2 and 2.1 import and export models
|