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 +4 -4
- data/canvas_qti_to_learnosity_converter.gemspec +1 -0
- data/lib/canvas_qti_to_learnosity_converter/convert.rb +302 -156
- data/lib/canvas_qti_to_learnosity_converter/export_writer.rb +21 -0
- data/lib/canvas_qti_to_learnosity_converter/questions/calculated.rb +4 -4
- data/lib/canvas_qti_to_learnosity_converter/questions/essay.rb +4 -4
- data/lib/canvas_qti_to_learnosity_converter/questions/file_upload.rb +4 -4
- data/lib/canvas_qti_to_learnosity_converter/questions/fill_the_blanks.rb +4 -5
- data/lib/canvas_qti_to_learnosity_converter/questions/matching.rb +4 -4
- data/lib/canvas_qti_to_learnosity_converter/questions/multiple_choice.rb +6 -6
- data/lib/canvas_qti_to_learnosity_converter/questions/numerical.rb +4 -4
- data/lib/canvas_qti_to_learnosity_converter/questions/question.rb +29 -0
- data/lib/canvas_qti_to_learnosity_converter/questions/short_answer.rb +4 -4
- data/lib/canvas_qti_to_learnosity_converter/questions/template_question.rb +4 -5
- data/lib/canvas_qti_to_learnosity_converter/questions/text_only.rb +4 -4
- data/lib/canvas_qti_to_learnosity_converter/version.rb +1 -1
- data/lib/canvas_qti_to_learnosity_converter.rb +0 -1
- metadata +17 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b87553f49f64e8fb54d2334edf92b467322364f3b32d8fa169c6f31cfef2d4be
|
4
|
+
data.tar.gz: 502a402267786ee374ced98d282bc4774b69243bee341e2ed35bd051dd6e7db9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 13688b93f41f22b5bfb9d1ef9a73c50828fa4c43f8a0c8b5e7102f3d5842b9d47d9735beaa5991babfc44374f3cb204d43a3a6d366ba71e6a13cbc8006ea4362
|
7
|
+
data.tar.gz: f74370f50a3e2a9c7052bc0e82a48ec6ed08ebc05151ffdc463768ff122b75cefafba0ee538b756e27f78ce6bf0fd3ce98539816cdefaa475a3aa0687e4c757c
|
@@ -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
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|
-
|
56
|
-
|
57
|
-
|
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
|
-
|
60
|
-
|
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
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
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
|
-
|
74
|
-
|
75
|
-
|
96
|
+
def build_quiz_from_qti_string(qti_string)
|
97
|
+
CanvasQtiQuiz.new(qti_string: qti_string)
|
98
|
+
end
|
76
99
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
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
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
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
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
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
|
-
|
100
|
-
|
101
|
-
|
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
|
-
|
104
|
-
|
105
|
-
else
|
106
|
-
learnosity_type = "question"
|
138
|
+
def clean_title(title)
|
139
|
+
title&.gsub(/["']/, "")
|
107
140
|
end
|
108
141
|
|
109
|
-
|
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
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
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
|
-
|
118
|
-
|
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
|
-
|
121
|
-
|
122
|
-
|
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
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
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
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
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
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
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
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
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
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
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
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
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
|
-
|
202
|
-
|
203
|
-
|
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
|
-
|
42
|
-
CanvasQtiToLearnosityConverter.add_files_to_assets(
|
40
|
+
def add_learnosity_assets(assets, path, learnosity)
|
41
|
+
process_assets!(
|
43
42
|
assets,
|
44
|
-
path
|
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
|
-
|
17
|
-
CanvasQtiToLearnosityConverter.add_files_to_assets(
|
15
|
+
def add_learnosity_assets(assets, path, learnosity)
|
16
|
+
process_assets!(
|
18
17
|
assets,
|
19
|
-
path
|
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
|
-
|
30
|
-
CanvasQtiToLearnosityConverter.add_files_to_assets(
|
28
|
+
def add_learnosity_assets(assets, path, learnosity)
|
29
|
+
process_assets!(
|
31
30
|
assets,
|
32
|
-
path
|
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
|
-
|
16
|
-
|
17
|
-
CanvasQtiToLearnosityConverter.add_files_to_assets(
|
14
|
+
def add_learnosity_assets(assets, path, learnosity)
|
15
|
+
process_assets!(
|
18
16
|
assets,
|
19
|
-
path
|
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
|
-
|
71
|
-
CanvasQtiToLearnosityConverter.add_files_to_assets(
|
69
|
+
def add_learnosity_assets(assets, path, learnosity)
|
70
|
+
process_assets!(
|
72
71
|
assets,
|
73
|
-
path
|
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
|
-
|
51
|
-
CanvasQtiToLearnosityConverter.add_files_to_assets(
|
49
|
+
def add_learnosity_assets(assets, path, learnosity)
|
50
|
+
process_assets!(
|
52
51
|
assets,
|
53
|
-
path
|
52
|
+
path,
|
54
53
|
learnosity[:stimulus]
|
55
54
|
)
|
56
55
|
|
57
56
|
learnosity[:options].each.with_index do |option, index|
|
58
|
-
|
57
|
+
process_assets!(
|
59
58
|
assets,
|
60
|
-
path
|
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
|
-
|
58
|
-
CanvasQtiToLearnosityConverter.add_files_to_assets(
|
56
|
+
def add_learnosity_assets(assets, path, learnosity)
|
57
|
+
process_assets!(
|
59
58
|
assets,
|
60
|
-
path
|
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
|
-
|
33
|
-
CanvasQtiToLearnosityConverter.add_files_to_assets(
|
31
|
+
def add_learnosity_assets(assets, path, learnosity)
|
32
|
+
process_assets!(
|
34
33
|
assets,
|
35
|
-
path
|
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
|
-
|
31
|
-
|
32
|
-
CanvasQtiToLearnosityConverter.add_files_to_assets(
|
29
|
+
def add_learnosity_assets(assets, path, learnosity)
|
30
|
+
process_assets!(
|
33
31
|
assets,
|
34
|
-
path
|
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
|
-
|
15
|
-
CanvasQtiToLearnosityConverter.add_files_to_assets(
|
13
|
+
def add_learnosity_assets(assets, path, learnosity)
|
14
|
+
process_assets!(
|
16
15
|
assets,
|
17
|
-
path
|
16
|
+
path,
|
18
17
|
learnosity[:content]
|
19
18
|
)
|
19
|
+
learnosity
|
20
20
|
end
|
21
21
|
end
|
22
22
|
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:
|
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:
|
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
|