qti 0.1.2 → 0.2.2

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