canvas_qti_to_learnosity_converter 2.4.1 → 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: 6c158942ecdb0d9e37f54528a9d2411d61827f2fc29eb7bfd9fea84cb41cfd7a
4
- data.tar.gz: f3073a832eee1b03125af16da109b4c4c9c419ac5dc9889435b1c591212d8b87
3
+ metadata.gz: b87553f49f64e8fb54d2334edf92b467322364f3b32d8fa169c6f31cfef2d4be
4
+ data.tar.gz: 502a402267786ee374ced98d282bc4774b69243bee341e2ed35bd051dd6e7db9
5
5
  SHA512:
6
- metadata.gz: de9f66b8dadb1c51c19733af8ca8f1bfd2e7317c215b3249a370da1bed63ce3aea7197b1a401101c3074f9819af77135dcd9605334d13f2227ec913f3c205f59
7
- data.tar.gz: 13bcaf6c6b73156f88929eb384e9d5e571144b505770eabedd1071be4de9c40ebad2befe0e2f19e8b9242014a7788c297fc4901ac5a19401ec2b201ac3def082
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,192 +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
-
47
- class CanvasQuestionTypeNotSupportedError < RuntimeError
48
- attr_reader :question_type
49
- def initialize(question_type)
50
- @question_type = question_type.to_s
51
- super("Unsupported question type #{@question_type}")
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
+ }
58
+
59
+ attr_accessor :items, :widgets, :item_banks, :assessments, :assets, :errors
60
+
61
+ def initialize
62
+ @items = []
63
+ @widgets = []
64
+ @item_banks = []
65
+ @assessments = []
66
+ @assets = {}
67
+ @errors = {}
68
+ @namespace = SecureRandom.uuid
52
69
  end
53
- end
54
70
 
55
- class CanvasQtiQuiz
56
- extend Forwardable
57
- def_delegators :@xml, :css
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
58
78
 
59
- def initialize(qti_string:)
60
- @xml = Nokogiri.XML(qti_string, &:noblanks)
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
61
85
  end
62
- end
63
86
 
87
+ class CanvasQtiQuiz
88
+ extend Forwardable
89
+ def_delegators :@xml, :css, :at_css
64
90
 
65
- def self.read_file(path)
66
- file = File.new path
67
- file.read
68
- ensure
69
- file.close
70
- # Do we need to unlink?
71
- end
91
+ def initialize(qti_string:)
92
+ @xml = Nokogiri.XML(qti_string, &:noblanks)
93
+ end
94
+ end
72
95
 
73
- def self.build_quiz_from_qti_string(qti_string)
74
- CanvasQtiQuiz.new(qti_string: qti_string)
75
- end
96
+ def build_quiz_from_qti_string(qti_string)
97
+ CanvasQtiQuiz.new(qti_string: qti_string)
98
+ end
76
99
 
77
- def self.build_quiz_from_file(path)
78
- qti_file = File.new path
79
- qti_string = qti_file.read
80
- CanvasQtiQuiz.new(qti_string: qti_string)
81
- ensure
82
- qti_file.close
83
- end
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
84
107
 
85
- def self.add_files_to_assets(assets, path, text)
86
- text.scan(/(%24|\$)IMS-CC-FILEBASE\1\/([^"]+)/) do |_delimiter, asset_path|
87
- decoded_path = URI::DEFAULT_PARSER.unescape(asset_path)
88
- assets[decoded_path] ||= []
89
- assets[decoded_path].push(path)
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
90
115
  end
91
- end
92
116
 
93
- def self.extract_type(xml)
94
- xml.css(%{ item > itemmetadata > qtimetadata >
95
- qtimetadatafield > fieldlabel:contains("question_type")})
96
- &.first&.next&.text&.to_sym
97
- end
117
+ def convert_item(qti_string:)
118
+ xml = Nokogiri.XML(qti_string, &:noblanks)
119
+ type = extract_type(xml)
120
+
121
+ if FEATURE_TYPES.include?(type)
122
+ learnosity_type = "feature"
123
+ else
124
+ learnosity_type = "question"
125
+ end
98
126
 
99
- def self.convert_item(qti_string:)
100
- xml = Nokogiri.XML(qti_string, &:noblanks)
101
- type = extract_type(xml)
127
+ question_class = TYPE_MAP[type]
128
+
129
+ if question_class
130
+ question = question_class.new(xml)
131
+ else
132
+ raise CanvasQuestionTypeNotSupportedError.new(type)
133
+ end
134
+
135
+ [learnosity_type, question]
136
+ end
102
137
 
103
- if FEATURE_TYPES.include?(type)
104
- learnosity_type = "feature"
105
- else
106
- learnosity_type = "question"
138
+ def clean_title(title)
139
+ title&.gsub(/["']/, "")
107
140
  end
108
141
 
109
- question_class = TYPE_MAP[type]
142
+ def convert_assessment(qti, path)
143
+ quiz = CanvasQtiQuiz.new(qti_string: qti)
144
+ assessment = quiz.at_css("assessment")
145
+ return nil unless assessment
110
146
 
111
- if question_class
112
- question = question_class.new(xml)
113
- else
114
- raise CanvasQuestionTypeNotSupportedError.new(type)
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
+ }
115
163
  end
116
164
 
117
- [learnosity_type, question]
118
- end
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
119
169
 
120
- def self.clean_title(title)
121
- title.gsub(/["']/, "")
122
- end
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 || '')
123
174
 
124
- def self.convert(qti, assets, errors)
125
- quiz = CanvasQtiQuiz.new(qti_string: qti)
126
- assessment = quiz.css("assessment")
127
- ident = assessment.attribute("ident").value
128
- assets[ident] = {}
129
- errors[ident] = []
130
-
131
- items = []
132
-
133
- quiz.css("item").each.with_index do |item, index|
134
- begin
135
- item_title = item.attribute("title").value
136
- learnosity_type, quiz_item = convert_item(qti_string: item.to_html)
137
-
138
- item = {
139
- title: item_title,
140
- type: learnosity_type,
141
- data: quiz_item.to_learnosity,
142
- 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,
143
184
  }
185
+ end
144
186
 
145
- items.push(item)
146
- path = [items.count - 1, :data]
147
-
148
- quiz_item.add_learnosity_assets(assets[ident], path)
149
- rescue CanvasQuestionTypeNotSupportedError => e
150
- errors[ident].push({
151
- index: index,
152
- error_type: "unsupported_question",
153
- question_type: e.question_type.to_s,
154
- message: e.message,
155
- })
156
- rescue StandardError => e
157
- errors[ident].push({
158
- index: index,
159
- error_type: e.class.to_s,
160
- message: e.message,
161
- })
187
+ def build_reference(ident = nil)
188
+ if ident.present?
189
+ Digest::UUID.uuid_v5(@namespace, ident)
190
+ else
191
+ SecureRandom.uuid
162
192
  end
163
193
  end
164
194
 
165
- {
166
- title: clean_title(assessment.attribute("title").value),
167
- ident: ident,
168
- items: items,
169
- }
170
- 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"
171
202
 
172
- def self.convert_qti_file(path)
173
- file = File.new(path)
174
- qti_string = file.read
175
- convert(qti_string)
176
- ensure
177
- file.close
178
- file.unlink
179
- end
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)
180
216
 
181
- def self.imscc_quiz_paths(parsed_manifest)
182
- parsed_manifest.css("resources > resource[type='imsqti_xmlv1p2'] > file").
183
- map { |entry| entry.attribute("href").value }
184
- end
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
185
243
 
186
- def self.convert_imscc_export(path)
187
- Zip::File.open(path) do |zip_file|
188
- entry = zip_file.find_entry("imsmanifest.xml")
189
- manifest = entry.get_input_stream.read
190
- parsed_manifest = Nokogiri.XML(manifest, &:noblanks)
191
- paths = imscc_quiz_paths(parsed_manifest)
192
-
193
- assets = {}
194
- errors = {}
195
- converted_assesments = paths.map do |qti_path|
196
- qti = zip_file.find_entry(qti_path).get_input_stream.read
197
- convert(qti, assets, errors)
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
198
260
  end
261
+ converted_item_refs
262
+ end
199
263
 
200
- {
201
- assessments: converted_assesments,
202
- assets: assets,
203
- errors: errors,
204
- }
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
272
+
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
278
+ end
279
+
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)
284
+ end
285
+ end
286
+
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
295
+ end
296
+ entry.css("file").first&.attribute("href")&.value
297
+ end
298
+
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
205
351
  end
206
352
  end
207
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.4.1"
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.4.1
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: 2023-09-21 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