canvas_qti_to_learnosity_converter 2.4.1 → 3.0.0

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