canvas_qti_to_learnosity_converter 2.5.0 → 3.0.0

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
  SHA256:
3
- metadata.gz: 367e7235a2a58b7fd63af3999e63cb960e34f46fa7f459c08d3608470d55b6c0
4
- data.tar.gz: 9e33eabe1f9ddecf970ee13fec9e9dc93e5062f658e5122dc90ea3c0a37a8677
3
+ metadata.gz: b87553f49f64e8fb54d2334edf92b467322364f3b32d8fa169c6f31cfef2d4be
4
+ data.tar.gz: 502a402267786ee374ced98d282bc4774b69243bee341e2ed35bd051dd6e7db9
5
5
  SHA512:
6
- metadata.gz: 62c09c119cab2a055a09f89dbfbb961d1a05f8922475940a86b276d77c16252b9ee56ff8f32aba4a52d9e2e87c2c0c7eeeced67bc0b98d59b11c117a4081589e
7
- data.tar.gz: 1735c52bf62ae1708e3a1a4eb72af38127eaebd7489e51a206496fa3207e7f350e7bd57e628c4eb8c32f59d4d435246216c61362f683785adfb40b5c67144448
6
+ metadata.gz: 13688b93f41f22b5bfb9d1ef9a73c50828fa4c43f8a0c8b5e7102f3d5842b9d47d9735beaa5991babfc44374f3cb204d43a3a6d366ba71e6a13cbc8006ea4362
7
+ data.tar.gz: f74370f50a3e2a9c7052bc0e82a48ec6ed08ebc05151ffdc463768ff122b75cefafba0ee538b756e27f78ce6bf0fd3ce98539816cdefaa475a3aa0687e4c757c
@@ -24,4 +24,5 @@ Gem::Specification.new do |spec|
24
24
  spec.add_development_dependency "rspec", "~> 3.0"
25
25
  spec.add_dependency "nokogiri"
26
26
  spec.add_dependency "rubyzip"
27
+ spec.add_dependency "activesupport"
27
28
  end
@@ -3,7 +3,12 @@ require "forwardable"
3
3
  require "ostruct"
4
4
  require "zip"
5
5
  require "uri"
6
+ require "active_support"
7
+ require "active_support/core_ext/digest/uuid"
8
+ require "active_support/core_ext/securerandom"
9
+ require "active_support/core_ext/object"
6
10
 
11
+ require "canvas_qti_to_learnosity_converter/export_writer"
7
12
  require "canvas_qti_to_learnosity_converter/questions/multiple_choice"
8
13
  require "canvas_qti_to_learnosity_converter/questions/short_answer"
9
14
  require "canvas_qti_to_learnosity_converter/questions/fill_the_blanks"
@@ -16,217 +21,333 @@ require "canvas_qti_to_learnosity_converter/questions/numerical"
16
21
  require "canvas_qti_to_learnosity_converter/questions/calculated"
17
22
 
18
23
  module CanvasQtiToLearnosityConverter
19
- FEATURE_TYPES = [ :text_only_question ]
20
- QUESTION_TYPES = [
21
- :multiple_choice_question,
22
- :true_false_question,
23
- :multiple_answers_question,
24
- :short_answer_question,
25
- :fill_in_multiple_blanks_question,
26
- :multiple_dropdowns_question,
27
- :matching_question,
28
- :essay_question,
29
- :file_upload_question,
30
- ]
31
-
32
- TYPE_MAP = {
33
- multiple_choice_question: MultipleChoiceQuestion,
34
- true_false_question: MultipleChoiceQuestion,
35
- multiple_answers_question: MultipleAnswersQuestion,
36
- short_answer_question: ShortAnswerQuestion,
37
- fill_in_multiple_blanks_question: FillTheBlanksQuestion,
38
- multiple_dropdowns_question: MultipleDropdownsQuestion,
39
- matching_question: MatchingQuestion,
40
- essay_question: EssayQuestion,
41
- file_upload_question: FileUploadQuestion,
42
- text_only_question: TextOnlyQuestion,
43
- numerical_question: NumericalQuestion,
44
- calculated_question: CalculatedQuestion,
45
-
46
- "cc.multiple_choice.v0p1": MultipleChoiceQuestion,
47
- "cc.multiple_response.v0p1": MultipleAnswersQuestion,
48
- "cc.fib.v0p1": ShortAnswerQuestion,
49
- "cc.true_false.v0p1": MultipleChoiceQuestion,
50
- "cc.essay.v0p1": EssayQuestion,
51
- }
52
-
53
- class CanvasQuestionTypeNotSupportedError < RuntimeError
54
- attr_reader :question_type
55
- def initialize(question_type)
56
- @question_type = question_type.to_s
57
- super("Unsupported question type #{@question_type}")
58
- end
59
- end
24
+ class Converter
25
+ FEATURE_TYPES = [ :text_only_question ]
26
+ QUESTION_TYPES = [
27
+ :multiple_choice_question,
28
+ :true_false_question,
29
+ :multiple_answers_question,
30
+ :short_answer_question,
31
+ :fill_in_multiple_blanks_question,
32
+ :multiple_dropdowns_question,
33
+ :matching_question,
34
+ :essay_question,
35
+ :file_upload_question,
36
+ ]
37
+
38
+ TYPE_MAP = {
39
+ multiple_choice_question: MultipleChoiceQuestion,
40
+ true_false_question: MultipleChoiceQuestion,
41
+ multiple_answers_question: MultipleAnswersQuestion,
42
+ short_answer_question: ShortAnswerQuestion,
43
+ fill_in_multiple_blanks_question: FillTheBlanksQuestion,
44
+ multiple_dropdowns_question: MultipleDropdownsQuestion,
45
+ matching_question: MatchingQuestion,
46
+ essay_question: EssayQuestion,
47
+ file_upload_question: FileUploadQuestion,
48
+ text_only_question: TextOnlyQuestion,
49
+ numerical_question: NumericalQuestion,
50
+ calculated_question: CalculatedQuestion,
51
+
52
+ "cc.multiple_choice.v0p1": MultipleChoiceQuestion,
53
+ "cc.multiple_response.v0p1": MultipleAnswersQuestion,
54
+ "cc.fib.v0p1": ShortAnswerQuestion,
55
+ "cc.true_false.v0p1": MultipleChoiceQuestion,
56
+ "cc.essay.v0p1": EssayQuestion,
57
+ }
60
58
 
61
- class CanvasQtiQuiz
62
- extend Forwardable
63
- def_delegators :@xml, :css
59
+ attr_accessor :items, :widgets, :item_banks, :assessments, :assets, :errors
64
60
 
65
- def initialize(qti_string:)
66
- @xml = Nokogiri.XML(qti_string, &:noblanks)
61
+ def initialize
62
+ @items = []
63
+ @widgets = []
64
+ @item_banks = []
65
+ @assessments = []
66
+ @assets = {}
67
+ @errors = {}
68
+ @namespace = SecureRandom.uuid
67
69
  end
68
- end
69
70
 
71
+ class CanvasQuestionTypeNotSupportedError < RuntimeError
72
+ attr_reader :question_type
73
+ def initialize(question_type)
74
+ @question_type = question_type.to_s
75
+ super("Unsupported question type #{@question_type}")
76
+ end
77
+ end
70
78
 
71
- def self.read_file(path)
72
- file = File.new path
73
- file.read
74
- ensure
75
- file.close
76
- # Do we need to unlink?
77
- end
78
-
79
- def self.build_quiz_from_qti_string(qti_string)
80
- CanvasQtiQuiz.new(qti_string: qti_string)
81
- end
79
+ class CanvasEntryTypeNotSupportedError < RuntimeError
80
+ attr_reader :question_type
81
+ def initialize(entry_type)
82
+ @entry_type = entry_type.to_s
83
+ super("Unsupported entry type #{@entry_type}")
84
+ end
85
+ end
82
86
 
83
- def self.build_quiz_from_file(path)
84
- qti_file = File.new path
85
- qti_string = qti_file.read
86
- CanvasQtiQuiz.new(qti_string: qti_string)
87
- ensure
88
- qti_file.close
89
- end
87
+ class CanvasQtiQuiz
88
+ extend Forwardable
89
+ def_delegators :@xml, :css, :at_css
90
90
 
91
- def self.add_files_to_assets(assets, path, text)
92
- text.scan(/(%24|\$)IMS-CC-FILEBASE\1\/([^"]+)/) do |_delimiter, asset_path|
93
- decoded_path = URI::DEFAULT_PARSER.unescape(asset_path)
94
- assets[decoded_path] ||= []
95
- assets[decoded_path].push(path)
91
+ def initialize(qti_string:)
92
+ @xml = Nokogiri.XML(qti_string, &:noblanks)
93
+ end
96
94
  end
97
- end
98
95
 
99
- def self.extract_type(xml)
100
- xml.css(%{ item > itemmetadata > qtimetadata >
101
- qtimetadatafield > fieldlabel:contains("question_type")})
102
- &.first&.next&.text&.to_sym ||
103
- xml.css(%{ item > itemmetadata > qtimetadata >
104
- qtimetadatafield > fieldlabel:contains("cc_profile")})
105
- &.first&.next&.text&.to_sym
106
- end
96
+ def build_quiz_from_qti_string(qti_string)
97
+ CanvasQtiQuiz.new(qti_string: qti_string)
98
+ end
107
99
 
108
- def self.convert_item(qti_string:)
109
- xml = Nokogiri.XML(qti_string, &:noblanks)
110
- type = extract_type(xml)
100
+ def build_quiz_from_file(path)
101
+ qti_file = File.new path
102
+ qti_string = qti_file.read
103
+ CanvasQtiQuiz.new(qti_string: qti_string)
104
+ ensure
105
+ qti_file.close
106
+ end
111
107
 
112
- if FEATURE_TYPES.include?(type)
113
- learnosity_type = "feature"
114
- else
115
- learnosity_type = "question"
108
+ def extract_type(xml)
109
+ xml.css(%{ item > itemmetadata > qtimetadata >
110
+ qtimetadatafield > fieldlabel:contains("question_type")})
111
+ &.first&.next&.text&.to_sym ||
112
+ xml.css(%{ item > itemmetadata > qtimetadata >
113
+ qtimetadatafield > fieldlabel:contains("cc_profile")})
114
+ &.first&.next&.text&.to_sym
116
115
  end
117
116
 
118
- question_class = TYPE_MAP[type]
117
+ def convert_item(qti_string:)
118
+ xml = Nokogiri.XML(qti_string, &:noblanks)
119
+ type = extract_type(xml)
119
120
 
120
- if question_class
121
- question = question_class.new(xml)
122
- else
123
- raise CanvasQuestionTypeNotSupportedError.new(type)
124
- end
121
+ if FEATURE_TYPES.include?(type)
122
+ learnosity_type = "feature"
123
+ else
124
+ learnosity_type = "question"
125
+ end
125
126
 
126
- [learnosity_type, question]
127
- end
127
+ question_class = TYPE_MAP[type]
128
128
 
129
- def self.clean_title(title)
130
- title.gsub(/["']/, "")
131
- end
129
+ if question_class
130
+ question = question_class.new(xml)
131
+ else
132
+ raise CanvasQuestionTypeNotSupportedError.new(type)
133
+ end
132
134
 
133
- def self.convert(qti, assets, errors)
134
- quiz = CanvasQtiQuiz.new(qti_string: qti)
135
- assessment = quiz.css("assessment")
136
- ident = assessment.attribute("ident").value
137
- assets[ident] = {}
138
- errors[ident] = []
135
+ [learnosity_type, question]
136
+ end
137
+
138
+ def clean_title(title)
139
+ title&.gsub(/["']/, "")
140
+ end
139
141
 
140
- items = []
142
+ def convert_assessment(qti, path)
143
+ quiz = CanvasQtiQuiz.new(qti_string: qti)
144
+ assessment = quiz.at_css("assessment")
145
+ return nil unless assessment
146
+
147
+ ident = assessment.attribute("ident")&.value
148
+ reference = build_reference(ident)
149
+ title = clean_title(assessment.attribute("title").value)
150
+
151
+ item_refs = convert_items(quiz, path)
152
+ @assessments <<
153
+ {
154
+ reference:,
155
+ title:,
156
+ data: {
157
+ items: item_refs.uniq.map { |ref| { reference: ref } },
158
+ config: { title: },
159
+ },
160
+ status: "published",
161
+ tags: {},
162
+ }
163
+ end
141
164
 
142
- quiz.css("item").each.with_index do |item, index|
143
- begin
144
- next if item.children.length == 0
165
+ def convert_item_bank(qti_string, path)
166
+ qti = CanvasQtiQuiz.new(qti_string:)
167
+ item_bank = qti.at_css("objectbank")
168
+ return nil unless item_bank
145
169
 
146
- item_title = item.attribute("title")&.value || ''
147
- learnosity_type, quiz_item = convert_item(qti_string: item.to_html)
170
+ ident = item_bank.attribute("ident")&.value
171
+ title = clean_title(qti.css(%{ objectbank > qtimetadata >
172
+ qtimetadatafield > fieldlabel:contains("bank_title")})
173
+ &.first&.next&.text || '')
148
174
 
149
- item = {
150
- title: item_title,
151
- type: learnosity_type,
152
- data: quiz_item.to_learnosity,
153
- dynamic_content_data: quiz_item.dynamic_content_data()
175
+ meta = {
176
+ original_item_bank_ref: ident,
177
+ }
178
+ item_refs = convert_items(qti, path, meta:, tags: { "Item Bank" => [title] })
179
+ @item_banks <<
180
+ {
181
+ title: title,
182
+ ident: ident,
183
+ item_refs: item_refs,
154
184
  }
185
+ end
155
186
 
156
- items.push(item)
157
- path = [items.count - 1, :data]
158
-
159
- quiz_item.add_learnosity_assets(assets[ident], path)
160
- rescue CanvasQuestionTypeNotSupportedError => e
161
- errors[ident].push({
162
- index: index,
163
- error_type: "unsupported_question",
164
- question_type: e.question_type.to_s,
165
- message: e.message,
166
- })
167
- rescue StandardError => e
168
- errors[ident].push({
169
- index: index,
170
- error_type: e.class.to_s,
171
- message: e.message,
172
- })
187
+ def build_reference(ident = nil)
188
+ if ident.present?
189
+ Digest::UUID.uuid_v5(@namespace, ident)
190
+ else
191
+ SecureRandom.uuid
173
192
  end
174
193
  end
175
194
 
176
- {
177
- title: clean_title(assessment.attribute("title").value),
178
- ident: ident,
179
- items: items,
180
- }
181
- end
195
+ def convert_items(qti, path, meta: {}, tags: {})
196
+ converted_item_refs = []
197
+ qti.css("item,bankentry_item,section").each.with_index do |item, index|
198
+ begin
199
+ ident = item.attribute("ident")&.value
200
+ if item.name == "section"
201
+ next if ident == "root_section"
202
+
203
+ item.css("sourcebank_ref").each do |sourcebank_ref|
204
+ item_refs = @items.select { |i| i.dig(:metadata, :original_item_bank_ref) == sourcebank_ref.text }.map { |i| i[:reference] }
205
+ converted_item_refs += item_refs
206
+ end
207
+ elsif item.name == "bankentry_item"
208
+ item_ref = item.attribute("item_ref")&.value
209
+ if item_ref
210
+ converted_item_refs.push(build_reference(item_ref))
211
+ end
212
+ elsif item.name == "item"
213
+ reference = build_reference(ident)
214
+ item_title = item.attribute("title")&.value || ''
215
+ learnosity_type, quiz_item = convert_item(qti_string: item.to_html)
216
+
217
+ item_widgets = [
218
+ {
219
+ type: learnosity_type,
220
+ data: quiz_item.convert(@assets, path),
221
+ reference: build_reference,
222
+ }
223
+ ]
224
+ @widgets += item_widgets
225
+
226
+ @items << {
227
+ title: item_title,
228
+ reference:,
229
+ metadata: meta.merge({ original_item_ref: ident }),
230
+ definition: {
231
+ widgets: item_widgets.map{ |w| { reference: w[:reference] } },
232
+ },
233
+ questions: item_widgets.select{ |w| w[:type] == "question" }.map{ |w| w[:reference] },
234
+ features: item_widgets.select{ |w| w[:type] == "feature" }.map{ |w| w[:reference] },
235
+ status: "published",
236
+ tags: tags,
237
+ type: learnosity_type,
238
+ dynamic_content_data: quiz_item.dynamic_content_data()
239
+ }
240
+
241
+ converted_item_refs.push(reference)
242
+ end
243
+
244
+ rescue CanvasQuestionTypeNotSupportedError => e
245
+ @errors[ident] ||= []
246
+ @errors[ident].push({
247
+ index: index,
248
+ error_type: "unsupported_question",
249
+ question_type: e.question_type.to_s,
250
+ message: e.message,
251
+ })
252
+ rescue StandardError => e
253
+ @errors[ident] ||= []
254
+ @errors[ident].push({
255
+ index: index,
256
+ error_type: e.class.to_s,
257
+ message: e.message,
258
+ })
259
+ end
260
+ end
261
+ converted_item_refs
262
+ end
182
263
 
183
- def self.convert_qti_file(path)
184
- file = File.new(path)
185
- qti_string = file.read
186
- convert(qti_string)
187
- ensure
188
- file.close
189
- file.unlink
190
- end
264
+ def convert_qti_file(path)
265
+ file = File.new(path)
266
+ qti_string = file.read
267
+ convert(qti_string)
268
+ ensure
269
+ file.close
270
+ file.unlink
271
+ end
191
272
 
192
- def self.imscc_quiz_paths(parsed_manifest)
193
- resources = parsed_manifest.css("resources > resource[type^='imsqti_xmlv1p2']")
194
- resources.map do |entry|
195
- resource_path(parsed_manifest, entry)
273
+ def imscc_quiz_paths(parsed_manifest)
274
+ resources = parsed_manifest.css("resources > resource[type^='imsqti_xmlv1p2']")
275
+ resources.map do |entry|
276
+ resource_path(parsed_manifest, entry)
277
+ end
196
278
  end
197
- end
198
279
 
199
- def self.resource_path(parsed_manifest, entry)
200
- # Use the Canvas non_cc_assignment qti path when possible. This works for both classic and new quizzes
201
- entry.css("dependency").each do |dependency|
202
- ref = dependency.attribute("identifierref").value
203
- parsed_manifest.css(%{resources > resource[identifier="#{ref}"] > file}).each do |file|
204
- path = file.attribute("href").value
205
- return path if path.match?(/^non_cc_assessments/)
280
+ def imscc_item_bank_paths(parsed_manifest)
281
+ resources = parsed_manifest.css("resources > resource[type='associatedcontent/imscc_xmlv1p1/learning-application-resource']")
282
+ resources.map do |entry|
283
+ resource_path(parsed_manifest, entry)
206
284
  end
207
285
  end
208
- entry.css("file").first&.attribute("href")&.value
209
- end
210
286
 
211
- def self.convert_imscc_export(path)
212
- Zip::File.open(path) do |zip_file|
213
- entry = zip_file.find_entry("imsmanifest.xml")
214
- manifest = entry.get_input_stream.read
215
- parsed_manifest = Nokogiri.XML(manifest, &:noblanks)
216
- paths = imscc_quiz_paths(parsed_manifest)
217
-
218
- assets = {}
219
- errors = {}
220
- converted_assesments = paths.map do |qti_path|
221
- qti = zip_file.find_entry(qti_path).get_input_stream.read
222
- convert(qti, assets, errors)
287
+ def resource_path(parsed_manifest, entry)
288
+ # Use the Canvas non_cc_assignment qti path when possible. This works for both classic and new quizzes
289
+ entry.css("dependency").each do |dependency|
290
+ ref = dependency.attribute("identifierref").value
291
+ parsed_manifest.css(%{resources > resource[identifier="#{ref}"] > file}).each do |file|
292
+ path = file.attribute("href").value
293
+ return path if path.match?(/^non_cc_assessments/)
294
+ end
223
295
  end
296
+ entry.css("file").first&.attribute("href")&.value
297
+ end
224
298
 
225
- {
226
- assessments: converted_assesments,
227
- assets: assets,
228
- errors: errors,
229
- }
299
+ def convert_imscc_export(path)
300
+ Zip::File.open(path) do |zip_file|
301
+ entry = zip_file.find_entry("imsmanifest.xml")
302
+ manifest = entry.get_input_stream.read
303
+ parsed_manifest = Nokogiri.XML(manifest, &:noblanks)
304
+
305
+ item_bank_paths = imscc_item_bank_paths(parsed_manifest)
306
+ item_bank_paths.each do |item_bank_path|
307
+ qti = zip_file.find_entry(item_bank_path).get_input_stream.read
308
+ convert_item_bank(qti, File.dirname(item_bank_path))
309
+ end
310
+
311
+ assessment_paths = imscc_quiz_paths(parsed_manifest)
312
+ assessment_paths.each do |qti_path|
313
+ qti = zip_file.find_entry(qti_path).get_input_stream.read
314
+ convert_assessment(qti, File.dirname(qti_path))
315
+ end
316
+
317
+ {
318
+ errors: @errors,
319
+ }
320
+ end
321
+ end
322
+
323
+ def generate_learnosity_export(input_path, output_path)
324
+ result = convert_imscc_export(input_path)
325
+
326
+ export_writer = ExportWriter.new(output_path)
327
+ export_writer.write_to_zip("export.json", { version: 2.0 })
328
+
329
+ @assessments.each do |activity|
330
+ export_writer.write_to_zip("activities/#{activity[:reference]}.json", activity)
331
+ end
332
+ @items.each do |item|
333
+ export_writer.write_to_zip("items/#{item[:reference]}.json", item)
334
+ end
335
+ @widgets.each do |widget|
336
+ export_writer.write_to_zip("#{widget[:type]}s/#{widget[:reference]}.json", widget)
337
+ end
338
+
339
+ Zip::File.open(input_path) do |input|
340
+ @assets.each do |source, destination|
341
+ source = source.gsub(/^\//, '')
342
+ asset = input.find_entry(source) || input.find_entry("web_resources/#{source}")
343
+ if asset
344
+ export_writer.write_asset_to_zip("assets/#{destination}", input.read(asset))
345
+ end
346
+ end
347
+ end
348
+ export_writer.close
349
+
350
+ result
230
351
  end
231
352
  end
232
353
  end
@@ -0,0 +1,21 @@
1
+ class ExportWriter
2
+ def initialize(temp_file)
3
+ @zip = Zip::File.open(temp_file.path, Zip::File::CREATE)
4
+ end
5
+
6
+ def close
7
+ @zip.close
8
+ end
9
+
10
+ def write_to_zip(filename, content)
11
+ @zip.get_output_stream(filename) do |file|
12
+ file << content.to_json
13
+ end
14
+ end
15
+
16
+ def write_asset_to_zip(filename, content)
17
+ @zip.get_output_stream(filename) do |file|
18
+ file << content
19
+ end
20
+ end
21
+ end
@@ -37,13 +37,13 @@ module CanvasQtiToLearnosityConverter
37
37
  }
38
38
  end
39
39
 
40
- def add_learnosity_assets(assets, path)
41
- learnosity = to_learnosity
42
- CanvasQtiToLearnosityConverter.add_files_to_assets(
40
+ def add_learnosity_assets(assets, path, learnosity)
41
+ process_assets!(
43
42
  assets,
44
- path + [:stimulus],
43
+ path,
45
44
  learnosity[:stimulus]
46
45
  )
46
+ learnosity
47
47
  end
48
48
 
49
49
  def dynamic_content_data()
@@ -12,13 +12,13 @@ module CanvasQtiToLearnosityConverter
12
12
  }
13
13
  end
14
14
 
15
- def add_learnosity_assets(assets, path)
16
- learnosity = to_learnosity
17
- CanvasQtiToLearnosityConverter.add_files_to_assets(
15
+ def add_learnosity_assets(assets, path, learnosity)
16
+ process_assets!(
18
17
  assets,
19
- path + [:stimulus],
18
+ path,
20
19
  learnosity[:stimulus]
21
20
  )
21
+ learnosity
22
22
  end
23
23
  end
24
24
  end
@@ -25,13 +25,13 @@ module CanvasQtiToLearnosityConverter
25
25
  }
26
26
  end
27
27
 
28
- def add_learnosity_assets(assets, path)
29
- learnosity = to_learnosity
30
- CanvasQtiToLearnosityConverter.add_files_to_assets(
28
+ def add_learnosity_assets(assets, path, learnosity)
29
+ process_assets!(
31
30
  assets,
32
- path + [:stimulus],
31
+ path,
33
32
  learnosity[:stimulus]
34
33
  )
34
+ learnosity
35
35
  end
36
36
  end
37
37
  end
@@ -11,14 +11,13 @@ module CanvasQtiToLearnosityConverter
11
11
  }
12
12
  end
13
13
 
14
- def add_learnosity_assets(assets, path)
15
- learnosity = to_learnosity
16
-
17
- CanvasQtiToLearnosityConverter.add_files_to_assets(
14
+ def add_learnosity_assets(assets, path, learnosity)
15
+ process_assets!(
18
16
  assets,
19
- path + [:template],
17
+ path,
20
18
  learnosity[:template]
21
19
  )
20
+ learnosity
22
21
  end
23
22
 
24
23
  def extract_validation()
@@ -66,13 +66,13 @@ module CanvasQtiToLearnosityConverter
66
66
  end
67
67
  end
68
68
 
69
- def add_learnosity_assets(assets, path)
70
- learnosity = to_learnosity
71
- CanvasQtiToLearnosityConverter.add_files_to_assets(
69
+ def add_learnosity_assets(assets, path, learnosity)
70
+ process_assets!(
72
71
  assets,
73
- path + [:stimulus],
72
+ path,
74
73
  learnosity[:stimulus]
75
74
  )
75
+ learnosity
76
76
  end
77
77
  end
78
78
  end
@@ -46,21 +46,21 @@ module CanvasQtiToLearnosityConverter
46
46
  }
47
47
  end
48
48
 
49
- def add_learnosity_assets(assets, path)
50
- learnosity = to_learnosity
51
- CanvasQtiToLearnosityConverter.add_files_to_assets(
49
+ def add_learnosity_assets(assets, path, learnosity)
50
+ process_assets!(
52
51
  assets,
53
- path + [:stimulus],
52
+ path,
54
53
  learnosity[:stimulus]
55
54
  )
56
55
 
57
56
  learnosity[:options].each.with_index do |option, index|
58
- CanvasQtiToLearnosityConverter.add_files_to_assets(
57
+ process_assets!(
59
58
  assets,
60
- path + [:options, index, "label"],
59
+ path,
61
60
  option["label"]
62
61
  )
63
62
  end
63
+ learnosity
64
64
  end
65
65
  end
66
66
 
@@ -53,13 +53,13 @@ module CanvasQtiToLearnosityConverter
53
53
  }
54
54
  end
55
55
 
56
- def add_learnosity_assets(assets, path)
57
- learnosity = to_learnosity
58
- CanvasQtiToLearnosityConverter.add_files_to_assets(
56
+ def add_learnosity_assets(assets, path, learnosity)
57
+ process_assets!(
59
58
  assets,
60
- path + [:stimulus],
59
+ path,
61
60
  learnosity[:stimulus]
62
61
  )
62
+ learnosity
63
63
  end
64
64
  end
65
65
  end
@@ -31,5 +31,34 @@ module CanvasQtiToLearnosityConverter
31
31
  def dynamic_content_data()
32
32
  {}
33
33
  end
34
+
35
+ def process_assets!(assets, path, text)
36
+ doc = Nokogiri::XML.fragment(text)
37
+ changed = false
38
+ doc.css("img").each do |node|
39
+ source = node["src"]
40
+ next if !source
41
+
42
+ source = URI::DEFAULT_PARSER.unescape(source)
43
+ if /^\$IMS-CC-FILEBASE\$(.*)/.match(source) || /^((?!https?:).*)/.match(source)
44
+ if source.start_with?("$IMS-CC-FILEBASE$")
45
+ path = ''
46
+ end
47
+ asset_path = $1
48
+ asset_path = asset_path.split("?").first.gsub(/^\//, '')
49
+ asset_path = File.join(path, asset_path)
50
+ clean_ext = File.extname(asset_path).gsub(/[^a-z0-9_.-]/i, '')
51
+ assets[asset_path] ||= "#{SecureRandom.uuid}#{clean_ext}"
52
+ node["src"] = "___EXPORT_ROOT___/assets/#{assets[asset_path]}"
53
+ changed = true
54
+ end
55
+ end
56
+ text.replace(doc.to_xml) if changed
57
+ end
58
+
59
+ def convert(assets, path)
60
+ object = to_learnosity
61
+ add_learnosity_assets(assets, path, object)
62
+ end
34
63
  end
35
64
  end
@@ -28,13 +28,13 @@ module CanvasQtiToLearnosityConverter
28
28
  }
29
29
  end
30
30
 
31
- def add_learnosity_assets(assets, path)
32
- learnosity = to_learnosity
33
- CanvasQtiToLearnosityConverter.add_files_to_assets(
31
+ def add_learnosity_assets(assets, path, learnosity)
32
+ process_assets!(
34
33
  assets,
35
- path + [:stimulus],
34
+ path,
36
35
  learnosity[:stimulus]
37
36
  )
37
+ learnosity
38
38
  end
39
39
  end
40
40
  end
@@ -26,14 +26,13 @@ module CanvasQtiToLearnosityConverter
26
26
  extract_mattext(template_node_list.first)
27
27
  end
28
28
 
29
- def add_learnosity_assets(assets, path)
30
- learnosity = to_learnosity
31
-
32
- CanvasQtiToLearnosityConverter.add_files_to_assets(
29
+ def add_learnosity_assets(assets, path, learnosity)
30
+ process_assets!(
33
31
  assets,
34
- path + [:template],
32
+ path,
35
33
  learnosity[:template]
36
34
  )
35
+ learnosity
37
36
  end
38
37
  end
39
38
  end
@@ -10,13 +10,13 @@ module CanvasQtiToLearnosityConverter
10
10
  }
11
11
  end
12
12
 
13
- def add_learnosity_assets(assets, path)
14
- learnosity = to_learnosity
15
- CanvasQtiToLearnosityConverter.add_files_to_assets(
13
+ def add_learnosity_assets(assets, path, learnosity)
14
+ process_assets!(
16
15
  assets,
17
- path + [:content],
16
+ path,
18
17
  learnosity[:content]
19
18
  )
19
+ learnosity
20
20
  end
21
21
  end
22
22
  end
@@ -1,3 +1,3 @@
1
1
  module CanvasQtiToLearnosityConverter
2
- VERSION = "2.5.0"
2
+ VERSION = "3.0.0"
3
3
  end
@@ -2,5 +2,4 @@ require 'canvas_qti_to_learnosity_converter/version'
2
2
  require 'canvas_qti_to_learnosity_converter/convert'
3
3
 
4
4
  module CanvasQtiToLearnosityConverter
5
- # Your code goes here...
6
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: canvas_qti_to_learnosity_converter
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.5.0
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Atomic Jolt
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2024-07-30 00:00:00.000000000 Z
12
+ date: 2024-08-27 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -95,6 +95,20 @@ dependencies:
95
95
  - - ">="
96
96
  - !ruby/object:Gem::Version
97
97
  version: '0'
98
+ - !ruby/object:Gem::Dependency
99
+ name: activesupport
100
+ requirement: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ type: :runtime
106
+ prerelease: false
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
98
112
  description:
99
113
  email:
100
114
  - support@atomicjolt.com
@@ -115,6 +129,7 @@ files:
115
129
  - canvas_qti_to_learnosity_converter.gemspec
116
130
  - lib/canvas_qti_to_learnosity_converter.rb
117
131
  - lib/canvas_qti_to_learnosity_converter/convert.rb
132
+ - lib/canvas_qti_to_learnosity_converter/export_writer.rb
118
133
  - lib/canvas_qti_to_learnosity_converter/questions/calculated.rb
119
134
  - lib/canvas_qti_to_learnosity_converter/questions/essay.rb
120
135
  - lib/canvas_qti_to_learnosity_converter/questions/file_upload.rb