qti 0.1.2 → 0.2.2

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: d28b9b4b803434ea56ecd453295db6a28b783b69
4
- data.tar.gz: 6ba9bef6b2282a140bc6093d9cf2f3f6f3f78f35
3
+ metadata.gz: 3c740d1b3ac0a16f4661dd5ec3fe25691e3aacc4
4
+ data.tar.gz: d487bd97b470c0948c4512e52cab356777c28674
5
5
  SHA512:
6
- metadata.gz: 7c4491ca81bc84b3e224b1965f5e5fd0e22e27335c85260ed1756a10fbedacf15db94c5a53af2bc35f5a75fee637a71485ff2460700357891d0324f9084c94fb
7
- data.tar.gz: a5ee171c6fe09d519be06d6dfdf3136e5ff364705c489bded21ac64180598ce2d5f4f4cd91538a4098f78e92a50ea12119f095baec44bcafeda987efad252b57
6
+ metadata.gz: af2cbeb0a5efc6aefd940b238b57a6bc5c273b0601096323ea6beece7137b6c1fcacaa28c6a98840704d656d22bd3214d2661874bc6ad796a4b68b4813caeb2a
7
+ data.tar.gz: 4b3b108554561f85eda5915f773b301ea396f6ec7e39ea2c46963498ab479f717b5f9f4198682c0bf86c73730f7f665cf330950fca3a69277c776fafb0bf25bd
@@ -0,0 +1,7 @@
1
+ class NullLogger < Logger
2
+ def initialize(*args)
3
+ end
4
+
5
+ def add(*args, &block)
6
+ end
7
+ end
data/lib/qti.rb CHANGED
@@ -46,6 +46,8 @@ module Qti
46
46
  end
47
47
  end
48
48
 
49
+ require 'active_support/core_ext/string'
50
+
49
51
  require 'qti/models/manifest'
50
52
  require 'qti/models/base'
51
53
 
@@ -59,3 +61,11 @@ require 'qti/v2/models/choices/simple_choice'
59
61
  require 'qti/v2/models/interactions/choice_interaction'
60
62
  require 'qti/v2/models/assessment_item'
61
63
  require 'qti/v2/models/assessment_test'
64
+
65
+ require 'zip'
66
+ require 'qti/exporter'
67
+
68
+ require 'qti/content_packaging'
69
+ require 'qti/assessment_item_exporter'
70
+
71
+ require 'null_logger'
@@ -0,0 +1,67 @@
1
+ module Qti
2
+ class AssessmentItemExporter
3
+ attr_reader :index, :assessment_item, :package_root_path
4
+
5
+ def initialize(index, assessment_item, args = {})
6
+ @index = index
7
+ @assessment_item = assessment_item
8
+ @package_root_path = args[:package_root_path] || '.'
9
+ end
10
+
11
+ def export
12
+ File.open(File.join(package_root_path, "item-#{index + 1}.xml"), 'wb') do |f|
13
+ f.write xml_string_for_assessment_item(assessment_item)
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def xml_string_for_assessment_item(assessment_item)
20
+ builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
21
+ xml.assessmentItem(assesment_item_attributes) do
22
+ xml.responseDeclaration('identifier' => 'RESPONSE',
23
+ 'cardinality' => cardinality_for_response(assessment_item),
24
+ 'baseType' => 'identifier') do
25
+ xml.correctResponse do
26
+ xml.value assessment_item.response
27
+ end
28
+ end
29
+ xml_assessment_item_body(xml, assessment_item)
30
+ end
31
+ end
32
+ builder.to_xml
33
+ end
34
+
35
+ def cardinality_for_response(assessment_item)
36
+ if assessment_item.response.is_a? Array
37
+ 'multiple'
38
+ else
39
+ 'single'
40
+ end
41
+ end
42
+
43
+ def xml_assessment_item_body(xml, assessment_item)
44
+ xml.itemBody do
45
+ xml.choiceInteraction(interaction_params(assessment_item.interaction)) do
46
+ xml.prompt assessment_item.interaction.prompt
47
+ assessment_item.interaction.choices.each do |choice|
48
+ xml.simpleChoice(choice.body, 'identifier' => choice.identifier, 'fixed' => choice.fixed)
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ def interaction_params(interaction)
55
+ { 'responseIdentifier' => 'RESPONSE',
56
+ 'shuffle' => interaction.shuffle,
57
+ 'maxChoices' => interaction.maxChoices }
58
+ end
59
+
60
+ def assesment_item_attributes
61
+ { 'xmlns' => 'http://www.imsglobal.org/xsd/imsqti_v2p2', 'xmlns:xi' => 'http://www.w3.org/2001/XInclude',
62
+ 'xmlns:m' => 'http://www.w3.org/1998/Math/MathML', 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
63
+ 'xsi:schemaLocation' => 'http://www.imsglobal.org/xsd/imsqti_v2p2
64
+ http://www.imsglobal.org/xsd/qti/qtiv2p2/imsqti_v2p2.xsd' }
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,15 @@
1
+ require 'dry-struct'
2
+
3
+ module Qti
4
+ module ContentPackaging
5
+ module Types
6
+ include Dry::Types.module
7
+ end
8
+ end
9
+ end
10
+
11
+ require 'qti/content_packaging/outcome_declaration'
12
+ require 'qti/content_packaging/simple_choice'
13
+ require 'qti/content_packaging/choice_interaction'
14
+ require 'qti/content_packaging/assessment_item'
15
+ require 'qti/content_packaging/assessment_test'
@@ -0,0 +1,10 @@
1
+ module Qti
2
+ module ContentPackaging
3
+ class AssessmentItem < Dry::Struct
4
+ attribute :identifier, Types::Strict::String
5
+ attribute :title, Types::Strict::String
6
+ attribute :interaction, ContentPackaging::ChoiceInteraction
7
+ attribute :response, Types::Strict::Array.member(String) | Types::Strict::String
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,9 @@
1
+ module Qti
2
+ module ContentPackaging
3
+ class AssessmentTest < Dry::Struct
4
+ attribute :title, Types::Strict::String
5
+ attribute :items, Types::Strict::Array.member(ContentPackaging::AssessmentItem)
6
+ attribute :outcome_declarations, Types::Strict::Array.member(ContentPackaging::OutcomeDeclaration)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,12 @@
1
+ module Qti
2
+ module ContentPackaging
3
+ class ChoiceInteraction < Dry::Struct
4
+ constructor_type :schema
5
+
6
+ attribute :prompt, Types::Strict::String
7
+ attribute :shuffle, Types::Strict::Bool.default(false)
8
+ attribute :maxChoices, Types::Coercible::Int
9
+ attribute :choices, Types::Strict::Array.member(ContentPackaging::SimpleChoice)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,10 @@
1
+ module Qti
2
+ module ContentPackaging
3
+ class OutcomeDeclaration < Dry::Struct
4
+ attribute :identifier, Types::Strict::String
5
+ attribute :baseType, Types::Strict::String
6
+ attribute :cardinality, Types::Strict::String
7
+ attribute :defaultValue, Types::Strict::String | Types::Coercible::Int | Types::Coercible::Float
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,11 @@
1
+ module Qti
2
+ module ContentPackaging
3
+ class SimpleChoice < Dry::Struct
4
+ constructor_type :schema
5
+
6
+ attribute :body, Types::Strict::String
7
+ attribute :identifier, Types::Strict::String
8
+ attribute :fixed, Types::Strict::Bool.default(true)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,168 @@
1
+ module Qti
2
+ class Exporter
3
+ attr_reader :assessment_test, :package_root_path, :exported_file_path, :logger
4
+
5
+ def initialize(assessment_test, args = {})
6
+ @logger = args[:logger] || NullLogger.new
7
+ @assessment_test = assessment_test
8
+ @package_root_path = args[:package_root_path] || '.'
9
+ @exported_file_path =
10
+ File.join(File.expand_path('..', package_root_path), File.basename(export_file_name)) + '.zip'
11
+ end
12
+
13
+ def export
14
+ Dir.mkdir(package_root_path) unless File.exist?(package_root_path)
15
+ create_assessment_xml
16
+ create_imsmanifest_xml
17
+ export_items
18
+ compress_package
19
+ end
20
+
21
+ private
22
+
23
+ def export_file_name
24
+ @export_file_name ||= "#{assessment_test.title.camelcase.gsub(/\s+/, '')}#{file_timestamp}"
25
+ end
26
+
27
+ def file_timestamp
28
+ Time.now.utc.strftime('%Y%m%d%H%M%S')
29
+ end
30
+
31
+ def create_assessment_xml
32
+ builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
33
+ xml.assessmentTest(assesment_test_attributes(title: assessment_test.title)) do
34
+ outcome_declarations(xml, assessment_test)
35
+ xml.testPart do
36
+ xml_assessment_section(xml)
37
+ end
38
+ end
39
+ end
40
+
41
+ File.open(File.join(package_root_path, 'assessment.xml'), 'wb') do |f|
42
+ f.write builder.to_xml
43
+ end
44
+ end
45
+
46
+ def outcome_declarations(xml, assessment_test)
47
+ assessment_test.outcome_declarations.each do |outcome_declaration|
48
+ xml.outcomeDeclaration('baseType' => outcome_declaration.baseType,
49
+ 'cardinality' => outcome_declaration.cardinality,
50
+ 'identifier' => outcome_declaration.identifier) do
51
+ xml.defaultValue do
52
+ xml.value outcome_declaration.defaultValue
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ def xml_assessment_section(xml)
59
+ xml.assessmentSection('identifier' => 'S1', 'visible' => 'true', 'title' => 'Section 1') do
60
+ assessment_test.items.each_with_index do |item, index|
61
+ xml.assessmentItemRef('identifier' => item.identifier, 'href' => "item-#{index + 1}.xml")
62
+ end
63
+ end
64
+ end
65
+
66
+ def create_imsmanifest_xml
67
+ builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
68
+ xml.manifest(imsmanifest_attributes) do
69
+ manifest_metadata(xml)
70
+ manifest_resources(xml)
71
+ end
72
+ end
73
+
74
+ File.open(File.join(package_root_path, 'imsmanifest.xml'), 'wb') do |f|
75
+ f.write builder.to_xml
76
+ end
77
+ end
78
+
79
+ def manifest_metadata(xml)
80
+ xml.metadata do
81
+ xml.schema 'QTIv2.2 Package'
82
+ xml.schemaversion '1.0.0'
83
+ learning_object_metadata(xml)
84
+ end
85
+ end
86
+
87
+ def learning_object_metadata(xml)
88
+ imsmd_ns = xml['imsmd']
89
+
90
+ imsmd_ns.lom do
91
+ imsmd_ns.general do
92
+ imsmd_ns.identifier do
93
+ imsmd_ns.entry 'FB-02'
94
+ end
95
+ imsmd_ns.title do
96
+ imsmd_ns.string(assessment_test.title, 'language' => 'en')
97
+ end
98
+ imsmd_ns.language 'en'
99
+ imsmd_ns.description do
100
+ imsmd_ns.string('Instructure QTI package.' \
101
+ 'Feedback XML used as an example of unprocessable entity', 'language' => 'en')
102
+ end
103
+ end
104
+ keywords(xml)
105
+ end
106
+ end
107
+
108
+ def keywords(xml)
109
+ keywords = %w(feedback modal test inline block)
110
+ keywords.each do |keyword|
111
+ xml['imsmd'].keyword do
112
+ xml['imsmd'].string(keyword, 'language' => 'en')
113
+ end
114
+ end
115
+ end
116
+
117
+ def manifest_resources(xml)
118
+ xml.resources do
119
+ xml.resource('href' => 'assessment.xml', 'type' => 'imsqti_test_xmlv2p2', 'identifier' => 'TEST') do
120
+ xml.file('href' => 'assessment.xml')
121
+ end
122
+ end
123
+ end
124
+
125
+ def export_items
126
+ assessment_test.items.each_with_index do |assessment_item, index|
127
+ Qti::AssessmentItemExporter.new(index, assessment_item, package_root_path: package_root_path).export
128
+ end
129
+ end
130
+
131
+ def assesment_test_attributes(args = {})
132
+ { 'xmlns' => 'http://www.imsglobal.org/xsd/imsqti_v2p2', 'xmlns:xi' => 'http://www.w3.org/2001/XInclude',
133
+ 'xmlns:m' => 'http://www.w3.org/1998/Math/MathML', 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
134
+ 'xsi:schemaLocation' => 'http://www.imsglobal.org/xsd/imsqti_v2p2' \
135
+ 'http://www.imsglobal.org/xsd/qti/qtiv2p2/imsqti_v2p2.xsd' }.merge(args)
136
+ end
137
+
138
+ def imsmanifest_attributes
139
+ { 'xmlns' => 'http://www.imsglobal.org/xsd/imscp_v1p1', 'xmlns:imsmd' => 'http://ltsc.ieee.org/xsd/LOM',
140
+ 'xmlns:imsqti' => 'http://www.imsglobal.org/xsd/imsqti_v2p2', 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
141
+ 'xsi:schemaLocation' => 'http://www.imsglobal.org/xsd/imscp_v1p1 ' \
142
+ 'http://www.imsglobal.org/xsd/qti/qtiv2p2/qtiv2p2_imscpv1p2_v1p0.xsd' \
143
+ 'http://ltsc.ieee.org/xsd/LOM http://www.imsglobal.org/xsd/imsmd_loose_v1p3p2.xsd' \
144
+ 'http://www.imsglobal.org/xsd/imsqti_metadata_v2p2' \
145
+ 'http://www.imsglobal.org/xsd/qti/qtiv2p2/imsqti_metadata_v2p2.xsd' }
146
+ end
147
+
148
+ def compress_package
149
+ Zip::File.open(exported_file_path, 'w') do |zipfile|
150
+ add_all_files(zipfile, package_root_path)
151
+ end
152
+
153
+ FileUtils.rm_rf(package_root_path)
154
+ exported_file_path
155
+ end
156
+
157
+ def add_all_files(zipfile, package_root_path)
158
+ Dir["#{package_root_path}/**/**"].each do |file|
159
+ begin
160
+ entry = file.sub(package_root_path + '/', '')
161
+ zipfile.add(entry, file)
162
+ rescue Zip::EntryExistsError
163
+ logger.info("#{file} already exists")
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,15 @@
1
+ module Qti
2
+ class ItemExporterFactory
3
+ EXPORTER_CLASS = { 'TrueFalse' => TrueFalseItemExporter }.freeze
4
+
5
+ def self.create(item, args = {})
6
+ exporter_class = EXPORTER_CLASS[item.interaction_type] || NullExporter
7
+ exporter_class.new(item, args)
8
+ end
9
+ end
10
+
11
+ class NullExporter < ItemExporter
12
+ def export
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,7 @@
1
+ module Qti
2
+ class TrueFalseItemExporter < ItemExporter
3
+ def export
4
+ xml_string_for_item(item)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,55 @@
1
+ require 'spec_helper'
2
+
3
+ describe Qti::Exporter do
4
+ let(:assessment_test) do
5
+ Qti::ContentPackaging::AssessmentTest.new(
6
+ title: 'Example test',
7
+ items: [
8
+ Qti::ContentPackaging::AssessmentItem.new(
9
+ identifier: '1', title: 'Question 1', response: 'true',
10
+ interaction: Qti::ContentPackaging::ChoiceInteraction.new(
11
+ prompt: 'Is 1+1 equals 2?', maxChoices: 1,
12
+ choices: [
13
+ Qti::ContentPackaging::SimpleChoice.new(
14
+ identifier: 'true', body: 'True'
15
+ ),
16
+ Qti::ContentPackaging::SimpleChoice.new(
17
+ identifier: 'false', body: 'False'
18
+ )
19
+ ]
20
+ )
21
+ )
22
+ ],
23
+ outcome_declarations: [
24
+ Qti::ContentPackaging::OutcomeDeclaration.new(
25
+ identifier: 'TEST_total', baseType: 'float', cardinality: 'single', defaultValue: 0.0
26
+ ),
27
+ Qti::ContentPackaging::OutcomeDeclaration.new(
28
+ identifier: 'S1', baseType: 'float', cardinality: 'single', defaultValue: 0.0
29
+ )
30
+ ]
31
+ )
32
+ end
33
+
34
+ it 'creates the zip file' do
35
+ dir = Dir.mktmpdir
36
+
37
+ exporter = Qti::Exporter.new(assessment_test, package_root_path: dir)
38
+ exporter.export
39
+
40
+ expect(File.exist?(exporter.exported_file_path)).to be_truthy
41
+ end
42
+
43
+ it 'contains an imsnanifest.xml and an assessment.xml files' do
44
+ dir = Dir.mktmpdir
45
+
46
+ exporter = Qti::Exporter.new(assessment_test, package_root_path: dir)
47
+ exporter.export
48
+
49
+ Zip::File.open(exporter.exported_file_path, 'r') do |zip_file|
50
+ expect(zip_file.glob('imsmanifest.xml').first).to be_present
51
+ expect(zip_file.glob('assessment.xml').first).to be_present
52
+ expect(zip_file.glob('item-1.xml').first).to be_present
53
+ end
54
+ end
55
+ end
@@ -1,4 +1,6 @@
1
1
  require 'bundler/setup'
2
+ require 'byebug'
3
+
2
4
  Bundler.setup
3
5
 
4
6
  require 'qti'
metadata CHANGED
@@ -1,15 +1,58 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: qti
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hannah Bottalla
8
+ - Robinson Rodríguez
8
9
  autorequire:
9
10
  bindir: bin
10
11
  cert_chain: []
11
- date: 2017-01-26 00:00:00.000000000 Z
12
+ date: 2017-03-06 00:00:00.000000000 Z
12
13
  dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activesupport
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: 4.2.6
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: 4.2.6
28
+ - !ruby/object:Gem::Dependency
29
+ name: dry-struct
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: 0.2.1
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: 0.2.1
42
+ - !ruby/object:Gem::Dependency
43
+ name: rubyzip
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: 1.2.0
49
+ type: :runtime
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: 1.2.0
13
56
  - !ruby/object:Gem::Dependency
14
57
  name: bundler
15
58
  requirement: !ruby/object:Gem::Requirement
@@ -24,6 +67,20 @@ dependencies:
24
67
  - - "~>"
25
68
  - !ruby/object:Gem::Version
26
69
  version: '1.11'
70
+ - !ruby/object:Gem::Dependency
71
+ name: byebug
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - "~>"
75
+ - !ruby/object:Gem::Version
76
+ version: 9.0.5
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: 9.0.5
27
84
  - !ruby/object:Gem::Dependency
28
85
  name: rake
29
86
  requirement: !ruby/object:Gem::Requirement
@@ -111,16 +168,28 @@ dependencies:
111
168
  description:
112
169
  email:
113
170
  - hannah@instructure.com
114
- - eng@instructure.com
171
+ - rrodriguez-bd@instructure.com
172
+ - quizzes@instructure.com
115
173
  executables: []
116
174
  extensions: []
117
175
  extra_rdoc_files: []
118
176
  files:
119
177
  - README.md
120
178
  - Rakefile
179
+ - lib/null_logger.rb
121
180
  - lib/qti.rb
181
+ - lib/qti/assessment_item_exporter.rb
182
+ - lib/qti/content_packaging.rb
183
+ - lib/qti/content_packaging/assessment_item.rb
184
+ - lib/qti/content_packaging/assessment_test.rb
185
+ - lib/qti/content_packaging/choice_interaction.rb
186
+ - lib/qti/content_packaging/outcome_declaration.rb
187
+ - lib/qti/content_packaging/simple_choice.rb
188
+ - lib/qti/exporter.rb
189
+ - lib/qti/item_exporter_factory.rb
122
190
  - lib/qti/models/base.rb
123
191
  - lib/qti/models/manifest.rb
192
+ - lib/qti/true_false_item_exporter.rb
124
193
  - lib/qti/v1/models/assessment.rb
125
194
  - lib/qti/v1/models/assessment_item.rb
126
195
  - lib/qti/v1/models/base.rb
@@ -297,6 +366,7 @@ files:
297
366
  - spec/fixtures/tests/tests/rtest25.xml
298
367
  - spec/fixtures/tests/tests/rtest26.xml
299
368
  - spec/fixtures/tests/tests/rtest27.xml
369
+ - spec/qti/exporter_spec.rb
300
370
  - spec/qti/models/base_spec.rb
301
371
  - spec/qti/models/manifest_spec.rb
302
372
  - spec/qti/v1/models/assessment_item_spec.rb
@@ -496,6 +566,7 @@ test_files:
496
566
  - spec/fixtures/tests/tests/rtest25.xml
497
567
  - spec/fixtures/tests/tests/rtest26.xml
498
568
  - spec/fixtures/tests/tests/rtest27.xml
569
+ - spec/qti/exporter_spec.rb
499
570
  - spec/qti/models/base_spec.rb
500
571
  - spec/qti/models/manifest_spec.rb
501
572
  - spec/qti/v1/models/assessment_item_spec.rb