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 +4 -4
- data/canvas_qti_to_learnosity_converter.gemspec +1 -0
- data/lib/canvas_qti_to_learnosity_converter/convert.rb +297 -176
- 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,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
|
-
|
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
|
-
|
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
|
-
|
62
|
-
extend Forwardable
|
63
|
-
def_delegators :@xml, :css
|
59
|
+
attr_accessor :items, :widgets, :item_banks, :assessments, :assets, :errors
|
64
60
|
|
65
|
-
def initialize
|
66
|
-
@
|
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
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
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
|
-
|
84
|
-
|
85
|
-
|
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
|
-
|
92
|
-
|
93
|
-
|
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
|
-
|
100
|
-
|
101
|
-
|
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
|
-
|
109
|
-
|
110
|
-
|
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
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
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
|
-
|
117
|
+
def convert_item(qti_string:)
|
118
|
+
xml = Nokogiri.XML(qti_string, &:noblanks)
|
119
|
+
type = extract_type(xml)
|
119
120
|
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
121
|
+
if FEATURE_TYPES.include?(type)
|
122
|
+
learnosity_type = "feature"
|
123
|
+
else
|
124
|
+
learnosity_type = "question"
|
125
|
+
end
|
125
126
|
|
126
|
-
|
127
|
-
end
|
127
|
+
question_class = TYPE_MAP[type]
|
128
128
|
|
129
|
-
|
130
|
-
|
131
|
-
|
129
|
+
if question_class
|
130
|
+
question = question_class.new(xml)
|
131
|
+
else
|
132
|
+
raise CanvasQuestionTypeNotSupportedError.new(type)
|
133
|
+
end
|
132
134
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
135
|
+
[learnosity_type, question]
|
136
|
+
end
|
137
|
+
|
138
|
+
def clean_title(title)
|
139
|
+
title&.gsub(/["']/, "")
|
140
|
+
end
|
139
141
|
|
140
|
-
|
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
|
-
|
143
|
-
|
144
|
-
|
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
|
-
|
147
|
-
|
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
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
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
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
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
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
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
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
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
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
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
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
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
|
-
|
212
|
-
|
213
|
-
entry
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
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
|
-
|
227
|
-
|
228
|
-
|
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
|
-
|
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: 2024-
|
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
|