canvas_qti_to_learnosity_converter 1.0.0 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/canvas_qti_to_learnosity_converter/convert.rb +73 -146
- data/lib/canvas_qti_to_learnosity_converter/questions/essay.rb +21 -0
- data/lib/canvas_qti_to_learnosity_converter/questions/file_upload.rb +34 -0
- data/lib/canvas_qti_to_learnosity_converter/questions/fill_the_blanks.rb +62 -0
- data/lib/canvas_qti_to_learnosity_converter/questions/matching.rb +75 -0
- data/lib/canvas_qti_to_learnosity_converter/questions/multiple_choice.rb +89 -0
- data/lib/canvas_qti_to_learnosity_converter/questions/multiple_dropdowns.rb +55 -0
- data/lib/canvas_qti_to_learnosity_converter/questions/numerical.rb +65 -0
- data/lib/canvas_qti_to_learnosity_converter/questions/question.rb +19 -0
- data/lib/canvas_qti_to_learnosity_converter/questions/short_answer.rb +40 -0
- data/lib/canvas_qti_to_learnosity_converter/questions/template_question.rb +39 -0
- data/lib/canvas_qti_to_learnosity_converter/questions/text_only.rb +22 -0
- data/lib/canvas_qti_to_learnosity_converter/version.rb +1 -1
- metadata +13 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '052911e6b4ed78bd22f6f6f99af67f56e5aa42bb84f327cfe9da5090900ff3f8'
|
4
|
+
data.tar.gz: 8927eb57c5e1b4a75dd00b49775e9479573295c7b7ce1bb48565bf0763852ea7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9e435bae6d96255228cd1ccc74b1b98e96208f5962b860db9629d07d3de668539065cc5795a13f09bff7ace2b6b84f938ae449b5d0268788a0cf17b1a4a08646
|
7
|
+
data.tar.gz: ece35a13ae4485a6eb0518e472530426c121215720759058ebcda7ce0c2283ac9d94e72944dc392ee78987caaab85035d8b5f8173a10ee5453605ced1fda1e21
|
@@ -4,7 +4,30 @@ require "ostruct"
|
|
4
4
|
require "zip"
|
5
5
|
require "uri"
|
6
6
|
|
7
|
+
require "canvas_qti_to_learnosity_converter/questions/multiple_choice"
|
8
|
+
require "canvas_qti_to_learnosity_converter/questions/short_answer"
|
9
|
+
require "canvas_qti_to_learnosity_converter/questions/fill_the_blanks"
|
10
|
+
require "canvas_qti_to_learnosity_converter/questions/multiple_dropdowns"
|
11
|
+
require "canvas_qti_to_learnosity_converter/questions/matching"
|
12
|
+
require "canvas_qti_to_learnosity_converter/questions/essay"
|
13
|
+
require "canvas_qti_to_learnosity_converter/questions/file_upload"
|
14
|
+
require "canvas_qti_to_learnosity_converter/questions/text_only"
|
15
|
+
require "canvas_qti_to_learnosity_converter/questions/numerical"
|
16
|
+
|
7
17
|
module CanvasQtiToLearnosityConverter
|
18
|
+
FEATURE_TYPES = [ :text_only_question ]
|
19
|
+
QUESTION_TYPES = [
|
20
|
+
:multiple_choice_question,
|
21
|
+
:true_false_question,
|
22
|
+
:multiple_answers_question,
|
23
|
+
:short_answer_question,
|
24
|
+
:fill_in_multiple_blanks_question,
|
25
|
+
:multiple_dropdowns_question,
|
26
|
+
:matching_question,
|
27
|
+
:essay_question,
|
28
|
+
:file_upload_question,
|
29
|
+
]
|
30
|
+
|
8
31
|
class CanvasQuestionTypeNotSupportedError < RuntimeError
|
9
32
|
end
|
10
33
|
|
@@ -17,33 +40,6 @@ module CanvasQtiToLearnosityConverter
|
|
17
40
|
end
|
18
41
|
end
|
19
42
|
|
20
|
-
class CanvasQtiQuizQuestion
|
21
|
-
extend Forwardable
|
22
|
-
def_delegators :@xml, :css
|
23
|
-
|
24
|
-
def initialize(qti_string:)
|
25
|
-
@xml = Nokogiri.XML(qti_string, &:noblanks)
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
class LearnosityQuestion < OpenStruct
|
30
|
-
end
|
31
|
-
|
32
|
-
class MultipleChoiceLearnosityQuestion < LearnosityQuestion
|
33
|
-
end
|
34
|
-
|
35
|
-
class MultipleAnswersLearnosityQuestion < LearnosityQuestion
|
36
|
-
end
|
37
|
-
|
38
|
-
class Learnosityitem
|
39
|
-
end
|
40
|
-
|
41
|
-
class LearnosityActivity
|
42
|
-
end
|
43
|
-
|
44
|
-
LEARNOSITY_TYPE_MAP = {
|
45
|
-
"mcq" => MultipleChoiceLearnosityQuestion
|
46
|
-
}
|
47
43
|
|
48
44
|
def self.read_file(path)
|
49
45
|
file = File.new path
|
@@ -65,94 +61,6 @@ module CanvasQtiToLearnosityConverter
|
|
65
61
|
qti_file.close
|
66
62
|
end
|
67
63
|
|
68
|
-
def self.build_item_from_file(path, item_type = nil)
|
69
|
-
file = File.new path
|
70
|
-
file_val = JSON.parse(file.read)
|
71
|
-
type = item_type || LEARNOSITY_TYPE_MAP[file_val["type"]]
|
72
|
-
type.new(file_val)
|
73
|
-
ensure
|
74
|
-
file.close
|
75
|
-
end
|
76
|
-
|
77
|
-
def self.extract_type(item)
|
78
|
-
item.css(%{ item > itemmetadata > qtimetadata >
|
79
|
-
qtimetadatafield > fieldlabel:contains("question_type")})
|
80
|
-
&.first&.next&.text&.to_sym
|
81
|
-
end
|
82
|
-
|
83
|
-
def self.extract_mattext(mattext_node)
|
84
|
-
mattext_node.content
|
85
|
-
end
|
86
|
-
|
87
|
-
def self.extract_stimulus(item)
|
88
|
-
mattext = item.css("item > presentation > material > mattext").first
|
89
|
-
extract_mattext(mattext)
|
90
|
-
end
|
91
|
-
|
92
|
-
def self.extract_multiple_choice_options(item)
|
93
|
-
choices = item.css("item > presentation > response_lid > render_choice > response_label")
|
94
|
-
choices.map do |choice|
|
95
|
-
ident = choice.attribute("ident").value
|
96
|
-
{
|
97
|
-
"value" => ident,
|
98
|
-
"label" => extract_mattext(choice.css("material > mattext").first),
|
99
|
-
}
|
100
|
-
end
|
101
|
-
end
|
102
|
-
|
103
|
-
def self.extract_response_id(item)
|
104
|
-
item.css("item > presentation > response_lid").attribute("ident").value
|
105
|
-
end
|
106
|
-
|
107
|
-
def self.extract_multiple_choice_validation(item)
|
108
|
-
resp_conditions = item.css("item > resprocessing > respcondition")
|
109
|
-
correct_condition = resp_conditions.select do |condition|
|
110
|
-
setvar = condition.css("setvar")
|
111
|
-
setvar.length === 1 && setvar.text === "100"
|
112
|
-
end.first
|
113
|
-
|
114
|
-
|
115
|
-
# TODO check for more than 1 element
|
116
|
-
correct_value = correct_condition.css("varequal").text
|
117
|
-
{
|
118
|
-
"scoring_type" => "exactMatch",
|
119
|
-
"valid_response" => {
|
120
|
-
"value" => [correct_value],
|
121
|
-
},
|
122
|
-
}
|
123
|
-
end
|
124
|
-
|
125
|
-
def self.extract_multiple_answers_validation(item)
|
126
|
-
correct_condition = item.css('item > resprocessing > respcondition[continue="No"] > conditionvar > and > varequal')
|
127
|
-
alt_responses = correct_condition.map(&:text)
|
128
|
-
{
|
129
|
-
"scoring_type" => "partialMatch",
|
130
|
-
"alt_responses" => alt_responses,
|
131
|
-
}
|
132
|
-
end
|
133
|
-
|
134
|
-
def self.convert_multiple_choice(item)
|
135
|
-
MultipleChoiceLearnosityQuestion.new({
|
136
|
-
stimulus: extract_stimulus(item),
|
137
|
-
options: extract_multiple_choice_options(item),
|
138
|
-
multiple_responses: false,
|
139
|
-
response_id: extract_response_id(item),
|
140
|
-
type: "mcq",
|
141
|
-
validation: extract_multiple_choice_validation(item),
|
142
|
-
})
|
143
|
-
end
|
144
|
-
|
145
|
-
def self.convert_multiple_answers(item)
|
146
|
-
MultipleAnswersLearnosityQuestion.new({
|
147
|
-
stimulus: extract_stimulus(item),
|
148
|
-
options: extract_multiple_choice_options(item),
|
149
|
-
multiple_responses: true,
|
150
|
-
response_id: extract_response_id(item),
|
151
|
-
type: "mcq",
|
152
|
-
validation: extract_multiple_answers_validation(item),
|
153
|
-
})
|
154
|
-
end
|
155
|
-
|
156
64
|
def self.add_files_to_assets(assets, path, text)
|
157
65
|
text.scan(/%24IMS-CC-FILEBASE%24\/([^"]+)/).flatten.each do |asset_path|
|
158
66
|
decoded_path = URI.unescape(asset_path)
|
@@ -161,35 +69,54 @@ module CanvasQtiToLearnosityConverter
|
|
161
69
|
end
|
162
70
|
end
|
163
71
|
|
164
|
-
def self.
|
165
|
-
|
72
|
+
def self.extract_type(xml)
|
73
|
+
xml.css(%{ item > itemmetadata > qtimetadata >
|
74
|
+
qtimetadatafield > fieldlabel:contains("question_type")})
|
75
|
+
&.first&.next&.text&.to_sym
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.convert_item(qti_string:)
|
79
|
+
xml = Nokogiri.XML(qti_string, &:noblanks)
|
80
|
+
type = extract_type(xml)
|
81
|
+
|
82
|
+
if FEATURE_TYPES.include?(type)
|
83
|
+
learnosity_type = "feature"
|
84
|
+
else
|
85
|
+
learnosity_type = "question"
|
86
|
+
end
|
166
87
|
|
167
88
|
question = case type
|
168
89
|
when :multiple_choice_question
|
169
|
-
|
90
|
+
MultipleChoiceQuestion.new(xml)
|
170
91
|
when :true_false_question
|
171
|
-
|
92
|
+
MultipleChoiceQuestion.new(xml)
|
172
93
|
when :multiple_answers_question
|
173
|
-
|
94
|
+
MultipleAnswersQuestion.new(xml)
|
95
|
+
when :short_answer_question
|
96
|
+
ShortAnswerQuestion.new(xml)
|
97
|
+
when :fill_in_multiple_blanks_question
|
98
|
+
FillTheBlanksQuestion.new(xml)
|
99
|
+
when :multiple_dropdowns_question
|
100
|
+
MultipleDropdownsQuestion.new(xml)
|
101
|
+
when :matching_question
|
102
|
+
MatchingQuestion.new(xml)
|
103
|
+
when :essay_question
|
104
|
+
EssayQuestion.new(xml)
|
105
|
+
when :file_upload_question
|
106
|
+
FileUploadQuestion.new(xml)
|
107
|
+
when :text_only_question
|
108
|
+
TextOnlyQuestion.new(xml)
|
109
|
+
when :numerical_question
|
110
|
+
NumericalQuestion.new(xml)
|
174
111
|
else
|
175
112
|
raise CanvasQuestionTypeNotSupportedError
|
176
113
|
end
|
177
114
|
|
178
|
-
|
179
|
-
|
180
|
-
[item_index, :stimulus],
|
181
|
-
question.stimulus
|
182
|
-
)
|
183
|
-
|
184
|
-
question.options.each.with_index do |option, index|
|
185
|
-
add_files_to_assets(
|
186
|
-
assets,
|
187
|
-
[item_index, :options, index, "label"],
|
188
|
-
option["label"]
|
189
|
-
)
|
190
|
-
end
|
115
|
+
[learnosity_type, question]
|
116
|
+
end
|
191
117
|
|
192
|
-
|
118
|
+
def self.clean_title(title)
|
119
|
+
title.gsub(/["']/, "")
|
193
120
|
end
|
194
121
|
|
195
122
|
def self.convert(qti, assets)
|
@@ -198,17 +125,23 @@ module CanvasQtiToLearnosityConverter
|
|
198
125
|
ident = assessment.attribute("ident").value
|
199
126
|
assets[ident] = {}
|
200
127
|
|
201
|
-
items =
|
128
|
+
items = []
|
129
|
+
|
130
|
+
quiz.css("item").each.with_index do |item, index|
|
202
131
|
begin
|
203
|
-
quiz_item =
|
204
|
-
|
132
|
+
learnosity_type, quiz_item = convert_item(qti_string: item.to_html)
|
133
|
+
|
134
|
+
items.push({type: learnosity_type, data: quiz_item.to_learnosity})
|
135
|
+
path = [items.count - 1, :data]
|
136
|
+
|
137
|
+
quiz_item.add_learnosity_assets(assets[ident], path)
|
205
138
|
rescue CanvasQuestionTypeNotSupportedError
|
206
139
|
nil
|
207
140
|
end
|
208
|
-
end
|
141
|
+
end
|
209
142
|
|
210
143
|
{
|
211
|
-
title: assessment.attribute("title").value,
|
144
|
+
title: clean_title(assessment.attribute("title").value),
|
212
145
|
ident: ident,
|
213
146
|
items: items,
|
214
147
|
}
|
@@ -228,12 +161,6 @@ module CanvasQtiToLearnosityConverter
|
|
228
161
|
map { |entry| entry.attribute("href").value }
|
229
162
|
end
|
230
163
|
|
231
|
-
def self.to_native_types(activity)
|
232
|
-
clone = activity.clone
|
233
|
-
clone[:items] = activity[:items].map(&:to_h)
|
234
|
-
clone
|
235
|
-
end
|
236
|
-
|
237
164
|
def self.convert_imscc_export(path)
|
238
165
|
Zip::File.open(path) do |zip_file|
|
239
166
|
entry = zip_file.find_entry("imsmanifest.xml")
|
@@ -244,7 +171,7 @@ module CanvasQtiToLearnosityConverter
|
|
244
171
|
assets = {}
|
245
172
|
converted_assesments = paths.map do |qti_path|
|
246
173
|
qti = zip_file.find_entry(qti_path).get_input_stream.read
|
247
|
-
|
174
|
+
convert(qti, assets)
|
248
175
|
end
|
249
176
|
|
250
177
|
{
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require "canvas_qti_to_learnosity_converter/questions/question"
|
2
|
+
|
3
|
+
module CanvasQtiToLearnosityConverter
|
4
|
+
class EssayQuestion < QuizQuestion
|
5
|
+
def to_learnosity
|
6
|
+
{
|
7
|
+
type: "longtextV2",
|
8
|
+
stimulus: extract_stimulus(),
|
9
|
+
}
|
10
|
+
end
|
11
|
+
|
12
|
+
def add_learnosity_assets(assets, path)
|
13
|
+
learnosity = to_learnosity
|
14
|
+
CanvasQtiToLearnosityConverter.add_files_to_assets(
|
15
|
+
assets,
|
16
|
+
path + [:stimulus],
|
17
|
+
learnosity[:stimulus]
|
18
|
+
)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require "canvas_qti_to_learnosity_converter/questions/question"
|
2
|
+
|
3
|
+
module CanvasQtiToLearnosityConverter
|
4
|
+
class FileUploadQuestion < QuizQuestion
|
5
|
+
def to_learnosity
|
6
|
+
{
|
7
|
+
type: "fileupload",
|
8
|
+
stimulus: extract_stimulus(),
|
9
|
+
allow_pdf: true,
|
10
|
+
allow_jpg: true,
|
11
|
+
allow_gif: true,
|
12
|
+
allow_png: true,
|
13
|
+
allow_csv: true,
|
14
|
+
allow_rtf: true,
|
15
|
+
allow_txt: true,
|
16
|
+
allow_xps: true,
|
17
|
+
allow_ms_word: true,
|
18
|
+
allow_ms_excel: true,
|
19
|
+
allow_ms_powerpoint: true,
|
20
|
+
allow_ms_publisher: true,
|
21
|
+
allow_open_office: true
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
def add_learnosity_assets(assets, path)
|
26
|
+
learnosity = to_learnosity
|
27
|
+
CanvasQtiToLearnosityConverter.add_files_to_assets(
|
28
|
+
assets,
|
29
|
+
path + [:stimulus],
|
30
|
+
learnosity[:stimulus]
|
31
|
+
)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require "canvas_qti_to_learnosity_converter/questions/template_question"
|
2
|
+
|
3
|
+
module CanvasQtiToLearnosityConverter
|
4
|
+
class FillTheBlanksQuestion < TemplateQuestion
|
5
|
+
def to_learnosity
|
6
|
+
{
|
7
|
+
type: "clozetext",
|
8
|
+
stimulus: "",
|
9
|
+
template: extract_template(),
|
10
|
+
validation: extract_validation(),
|
11
|
+
}
|
12
|
+
end
|
13
|
+
|
14
|
+
def add_learnosity_assets(assets, path)
|
15
|
+
learnosity = to_learnosity
|
16
|
+
|
17
|
+
CanvasQtiToLearnosityConverter.add_files_to_assets(
|
18
|
+
assets,
|
19
|
+
path + [:template],
|
20
|
+
learnosity[:template]
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
def extract_validation()
|
25
|
+
template = get_template()
|
26
|
+
|
27
|
+
responses = extract_template_values(template).map do |name|
|
28
|
+
result = @xml.css(%{item > presentation >
|
29
|
+
response_lid[ident="response_#{name}"] > render_choice material >
|
30
|
+
mattext}).map do |node|
|
31
|
+
extract_mattext(node)
|
32
|
+
end
|
33
|
+
|
34
|
+
if result.empty?
|
35
|
+
nil
|
36
|
+
else
|
37
|
+
result
|
38
|
+
end
|
39
|
+
end.compact
|
40
|
+
|
41
|
+
all_responses = []
|
42
|
+
create_responses(responses, 0, all_responses, [])
|
43
|
+
|
44
|
+
{
|
45
|
+
"scoring_type" => "partialMatch",
|
46
|
+
"valid_response" => all_responses.shift,
|
47
|
+
"alt_responses" => all_responses
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
def create_responses(blank_responses, depth, result, current_response)
|
52
|
+
if depth == blank_responses.count
|
53
|
+
result.push({ "value" => current_response })
|
54
|
+
return
|
55
|
+
end
|
56
|
+
|
57
|
+
blank_responses[depth].each do |possible_response|
|
58
|
+
create_responses(blank_responses, depth + 1, result, current_response + [possible_response])
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require "canvas_qti_to_learnosity_converter/questions/question"
|
2
|
+
|
3
|
+
module CanvasQtiToLearnosityConverter
|
4
|
+
class MatchingQuestion < QuizQuestion
|
5
|
+
def to_learnosity
|
6
|
+
{
|
7
|
+
type: "association",
|
8
|
+
stimulus: extract_stimulus(),
|
9
|
+
stimulus_list: extract_stimulus_list(),
|
10
|
+
validation: extract_validation(),
|
11
|
+
possible_responses: extract_responses(),
|
12
|
+
duplicate_responses: true,
|
13
|
+
}
|
14
|
+
end
|
15
|
+
|
16
|
+
def extract_stimulus_list()
|
17
|
+
@xml.css("item > presentation > response_lid > material > mattext").map do |node|
|
18
|
+
extract_mattext(node)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def extract_response_idents()
|
23
|
+
@xml.css("item > presentation > response_lid").map do |node|
|
24
|
+
node.attribute("ident").text
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def extract_valid_label_idents(valid_response_idents)
|
29
|
+
valid_response_idents.map do |ident|
|
30
|
+
@xml.css(%{item > resprocessing > respcondition varequal[respident="#{ident}"]}).map do |node|
|
31
|
+
node.content
|
32
|
+
end
|
33
|
+
end.flatten
|
34
|
+
end
|
35
|
+
|
36
|
+
def extract_validation()
|
37
|
+
valid_response_idents = extract_response_idents()
|
38
|
+
valid_label_idents = extract_valid_label_idents(valid_response_idents)
|
39
|
+
|
40
|
+
valid_idents = Hash[valid_response_idents.zip(valid_label_idents)]
|
41
|
+
|
42
|
+
valid_responses = valid_idents.map do |response_ident, label_ident|
|
43
|
+
@xml.css(%{item > presentation >
|
44
|
+
response_lid[ident="#{response_ident}"]
|
45
|
+
response_label[ident="#{label_ident}"] > material > mattext}).map do |node|
|
46
|
+
extract_mattext(node)
|
47
|
+
end
|
48
|
+
end.flatten
|
49
|
+
|
50
|
+
{
|
51
|
+
"scoring_type" => "partialMatch",
|
52
|
+
"valid_response" => {
|
53
|
+
"value" => valid_responses
|
54
|
+
}
|
55
|
+
}
|
56
|
+
end
|
57
|
+
|
58
|
+
def extract_responses()
|
59
|
+
response_node = @xml.css("item > presentation > response_lid").first
|
60
|
+
|
61
|
+
response_node.css("response_label mattext").map do |node|
|
62
|
+
extract_mattext(node)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def add_learnosity_assets(assets, path)
|
67
|
+
learnosity = to_learnosity
|
68
|
+
CanvasQtiToLearnosityConverter.add_files_to_assets(
|
69
|
+
assets,
|
70
|
+
path + [:stimulus],
|
71
|
+
learnosity[:stimulus]
|
72
|
+
)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require "canvas_qti_to_learnosity_converter/questions/question"
|
2
|
+
|
3
|
+
module CanvasQtiToLearnosityConverter
|
4
|
+
class MultipleChoiceQuestion < QuizQuestion
|
5
|
+
def extract_options()
|
6
|
+
choices = @xml.css("item > presentation > response_lid > render_choice > response_label")
|
7
|
+
choices.map do |choice|
|
8
|
+
ident = choice.attribute("ident").value
|
9
|
+
{
|
10
|
+
"value" => ident,
|
11
|
+
"label" => extract_mattext(choice.css("material > mattext").first),
|
12
|
+
}
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def extract_response_id()
|
17
|
+
@xml.css("item > presentation > response_lid").attribute("ident").value
|
18
|
+
end
|
19
|
+
|
20
|
+
def extract_validation()
|
21
|
+
resp_conditions = @xml.css("item > resprocessing > respcondition")
|
22
|
+
correct_condition = resp_conditions.select do |condition|
|
23
|
+
setvar = condition.css("setvar")
|
24
|
+
setvar.length === 1 && setvar.text === "100"
|
25
|
+
end.first
|
26
|
+
|
27
|
+
# TODO check for more than 1 element
|
28
|
+
correct_value = correct_condition.css("varequal").text
|
29
|
+
{
|
30
|
+
"scoring_type" => "exactMatch",
|
31
|
+
"valid_response" => {
|
32
|
+
"value" => [correct_value],
|
33
|
+
},
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_learnosity
|
38
|
+
{
|
39
|
+
stimulus: extract_stimulus(),
|
40
|
+
options: extract_options(),
|
41
|
+
multiple_responses: false,
|
42
|
+
response_id: extract_response_id(),
|
43
|
+
type: "mcq",
|
44
|
+
validation: extract_validation(),
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
def add_learnosity_assets(assets, path)
|
49
|
+
learnosity = to_learnosity
|
50
|
+
CanvasQtiToLearnosityConverter.add_files_to_assets(
|
51
|
+
assets,
|
52
|
+
path + [:stimulus],
|
53
|
+
learnosity[:stimulus]
|
54
|
+
)
|
55
|
+
|
56
|
+
learnosity[:options].each.with_index do |option, index|
|
57
|
+
CanvasQtiToLearnosityConverter.add_files_to_assets(
|
58
|
+
assets,
|
59
|
+
path + [:options, index, "label"],
|
60
|
+
option["label"]
|
61
|
+
)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class MultipleAnswersQuestion < MultipleChoiceQuestion
|
67
|
+
def to_learnosity
|
68
|
+
{
|
69
|
+
stimulus: extract_stimulus(),
|
70
|
+
options: extract_options(),
|
71
|
+
multiple_responses: true,
|
72
|
+
response_id: extract_response_id(),
|
73
|
+
type: "mcq",
|
74
|
+
validation: extract_validation()
|
75
|
+
}
|
76
|
+
end
|
77
|
+
|
78
|
+
def extract_validation()
|
79
|
+
correct_condition = @xml.css('item > resprocessing >
|
80
|
+
respcondition[continue="No"] > conditionvar >
|
81
|
+
and > varequal')
|
82
|
+
alt_responses = correct_condition.map(&:text)
|
83
|
+
{
|
84
|
+
"scoring_type" => "partialMatch",
|
85
|
+
"alt_responses" => alt_responses,
|
86
|
+
}
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require "canvas_qti_to_learnosity_converter/questions/template_question"
|
2
|
+
|
3
|
+
module CanvasQtiToLearnosityConverter
|
4
|
+
class MultipleDropdownsQuestion < TemplateQuestion
|
5
|
+
def to_learnosity
|
6
|
+
{
|
7
|
+
type: "clozedropdown",
|
8
|
+
stimulus: "",
|
9
|
+
template: extract_template(),
|
10
|
+
validation: extract_validation(),
|
11
|
+
possible_responses: extract_responses(),
|
12
|
+
}
|
13
|
+
end
|
14
|
+
|
15
|
+
def extract_validation()
|
16
|
+
template = get_template()
|
17
|
+
|
18
|
+
valid_responses = extract_template_values(template).map do |name|
|
19
|
+
correct_ident = @xml.css(%{item > resprocessing > respcondition >
|
20
|
+
conditionvar > varequal[respident="response_#{name}"]}).first&.content
|
21
|
+
|
22
|
+
@xml.css(%{item > presentation >
|
23
|
+
response_lid[ident="response_#{name}"] response_label}).map do |node|
|
24
|
+
text = extract_mattext(node.css("mattext").first)
|
25
|
+
ident = node.attribute("ident").text
|
26
|
+
ident == correct_ident ? text : nil
|
27
|
+
end.compact
|
28
|
+
end.flatten
|
29
|
+
|
30
|
+
{
|
31
|
+
"scoring_type" => "partialMatch",
|
32
|
+
"valid_response" => {
|
33
|
+
"value" => valid_responses,
|
34
|
+
}
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
def extract_responses()
|
39
|
+
template = get_template()
|
40
|
+
|
41
|
+
extract_template_values(template).map do |name|
|
42
|
+
result = @xml.css(%{item > presentation >
|
43
|
+
response_lid[ident="response_#{name}"] > render_choice mattext}).map do |node|
|
44
|
+
extract_mattext(node)
|
45
|
+
end
|
46
|
+
|
47
|
+
if result.empty?
|
48
|
+
nil
|
49
|
+
else
|
50
|
+
result
|
51
|
+
end
|
52
|
+
end.compact
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require "canvas_qti_to_learnosity_converter/questions/question"
|
2
|
+
|
3
|
+
module CanvasQtiToLearnosityConverter
|
4
|
+
class NumericalQuestion < QuizQuestion
|
5
|
+
def to_learnosity
|
6
|
+
{
|
7
|
+
is_math: true,
|
8
|
+
type: "formulaV2",
|
9
|
+
stimulus: extract_stimulus(),
|
10
|
+
template: "{{response}}",
|
11
|
+
validation: extract_validation(),
|
12
|
+
}
|
13
|
+
end
|
14
|
+
|
15
|
+
def extract_validation()
|
16
|
+
response_mins = @xml.css('item > resprocessing >
|
17
|
+
respcondition[continue="No"] > conditionvar vargte').map do |node|
|
18
|
+
node.content
|
19
|
+
end
|
20
|
+
|
21
|
+
response_maxs = @xml.css('item > resprocessing >
|
22
|
+
respcondition[continue="No"] > conditionvar varlte').map do |node|
|
23
|
+
node.content
|
24
|
+
end
|
25
|
+
|
26
|
+
answer_bounds = response_mins.zip(response_maxs).map do |bounds|
|
27
|
+
puts bounds.inspect
|
28
|
+
# Get the precision by counting the number of places after the decimal
|
29
|
+
precision = [
|
30
|
+
bounds.first.split(".").last.length,
|
31
|
+
bounds.last.split(".").last.length
|
32
|
+
].max
|
33
|
+
|
34
|
+
{
|
35
|
+
center: ((bounds.first.to_f + bounds.last.to_f) / 2.0).round(precision),
|
36
|
+
pm: ((bounds.first.to_f - bounds.last.to_f) / 2.0).round(precision).abs,
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
valid_answers = answer_bounds.map do |bounds|
|
41
|
+
{
|
42
|
+
"value" => [{
|
43
|
+
"method" => "equivValue",
|
44
|
+
"value" => "#{bounds[:center]}\\pm#{bounds[:pm]}",
|
45
|
+
}]
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
{
|
50
|
+
"scoring_type" => "exactMatch",
|
51
|
+
"valid_response" => valid_answers.shift,
|
52
|
+
"alt_responses" => valid_answers,
|
53
|
+
}
|
54
|
+
end
|
55
|
+
|
56
|
+
def add_learnosity_assets(assets, path)
|
57
|
+
learnosity = to_learnosity
|
58
|
+
CanvasQtiToLearnosityConverter.add_files_to_assets(
|
59
|
+
assets,
|
60
|
+
path + [:stimulus],
|
61
|
+
learnosity[:stimulus]
|
62
|
+
)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module CanvasQtiToLearnosityConverter
|
2
|
+
class QuizQuestion
|
3
|
+
extend Forwardable
|
4
|
+
def_delegators :@xml, :css
|
5
|
+
|
6
|
+
def initialize(xml)
|
7
|
+
@xml = xml
|
8
|
+
end
|
9
|
+
|
10
|
+
def extract_stimulus()
|
11
|
+
mattext = @xml.css("item > presentation > material > mattext").first
|
12
|
+
extract_mattext(mattext)
|
13
|
+
end
|
14
|
+
|
15
|
+
def extract_mattext(mattext_node)
|
16
|
+
mattext_node.content
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require "canvas_qti_to_learnosity_converter/questions/question"
|
2
|
+
|
3
|
+
module CanvasQtiToLearnosityConverter
|
4
|
+
# This is fill in the blank in the Canvas UI, but it is actually a short
|
5
|
+
# answer type.
|
6
|
+
class ShortAnswerQuestion < QuizQuestion
|
7
|
+
def extract_response_id()
|
8
|
+
@xml.css("item > presentation > response_str").attribute("ident").value
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_learnosity
|
12
|
+
{
|
13
|
+
type: "shorttext",
|
14
|
+
stimulus: extract_stimulus(),
|
15
|
+
validation: extract_validation(),
|
16
|
+
response_id: extract_response_id(),
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
def extract_validation()
|
21
|
+
correct_responses = @xml.css('item > resprocessing >
|
22
|
+
respcondition[continue="No"] > conditionvar > varequal')
|
23
|
+
correct_response = { "value" => correct_responses.shift.text }
|
24
|
+
{
|
25
|
+
"scoring_type" => "exactMatch",
|
26
|
+
"valid_response" => correct_response,
|
27
|
+
"alt_responses" => correct_responses.map { |res| { "value" => res.text } }
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
def add_learnosity_assets(assets, path)
|
32
|
+
learnosity = to_learnosity
|
33
|
+
CanvasQtiToLearnosityConverter.add_files_to_assets(
|
34
|
+
assets,
|
35
|
+
path + [:stimulus],
|
36
|
+
learnosity[:stimulus]
|
37
|
+
)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require "canvas_qti_to_learnosity_converter/questions/question"
|
2
|
+
|
3
|
+
module CanvasQtiToLearnosityConverter
|
4
|
+
class TemplateQuestion < QuizQuestion
|
5
|
+
def extract_template()
|
6
|
+
placeholders = @xml.css("item > presentation > response_lid > material >
|
7
|
+
mattext").map { |text| extract_mattext(text) }
|
8
|
+
|
9
|
+
template = get_template()
|
10
|
+
|
11
|
+
placeholders.each do |placeholder|
|
12
|
+
template.sub!("[#{placeholder}]", "{{response}}")
|
13
|
+
end
|
14
|
+
|
15
|
+
template
|
16
|
+
end
|
17
|
+
|
18
|
+
def extract_template_values(template)
|
19
|
+
template.scan(/\[([^\]]+)\]/).map do |capture_list|
|
20
|
+
capture_list.first
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def get_template()
|
25
|
+
template_node_list = @xml.css("item > presentation > material > mattext")
|
26
|
+
extract_mattext(template_node_list.first)
|
27
|
+
end
|
28
|
+
|
29
|
+
def add_learnosity_assets(assets, path)
|
30
|
+
learnosity = to_learnosity
|
31
|
+
|
32
|
+
CanvasQtiToLearnosityConverter.add_files_to_assets(
|
33
|
+
assets,
|
34
|
+
path + [:template],
|
35
|
+
learnosity[:template]
|
36
|
+
)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require "canvas_qti_to_learnosity_converter/questions/question"
|
2
|
+
|
3
|
+
module CanvasQtiToLearnosityConverter
|
4
|
+
class TextOnlyQuestion < QuizQuestion
|
5
|
+
def to_learnosity
|
6
|
+
{
|
7
|
+
type: "sharedpassage",
|
8
|
+
heading: "",
|
9
|
+
content: extract_stimulus(),
|
10
|
+
}
|
11
|
+
end
|
12
|
+
|
13
|
+
def add_learnosity_assets(assets, path)
|
14
|
+
learnosity = to_learnosity
|
15
|
+
CanvasQtiToLearnosityConverter.add_files_to_assets(
|
16
|
+
assets,
|
17
|
+
path + [:content],
|
18
|
+
learnosity[:content]
|
19
|
+
)
|
20
|
+
end
|
21
|
+
end
|
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: 1.0
|
4
|
+
version: 2.1.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: 2018-
|
12
|
+
date: 2018-08-10 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bundler
|
@@ -115,6 +115,17 @@ files:
|
|
115
115
|
- canvas_qti_to_learnosity_converter.gemspec
|
116
116
|
- lib/canvas_qti_to_learnosity_converter.rb
|
117
117
|
- lib/canvas_qti_to_learnosity_converter/convert.rb
|
118
|
+
- lib/canvas_qti_to_learnosity_converter/questions/essay.rb
|
119
|
+
- lib/canvas_qti_to_learnosity_converter/questions/file_upload.rb
|
120
|
+
- lib/canvas_qti_to_learnosity_converter/questions/fill_the_blanks.rb
|
121
|
+
- lib/canvas_qti_to_learnosity_converter/questions/matching.rb
|
122
|
+
- lib/canvas_qti_to_learnosity_converter/questions/multiple_choice.rb
|
123
|
+
- lib/canvas_qti_to_learnosity_converter/questions/multiple_dropdowns.rb
|
124
|
+
- lib/canvas_qti_to_learnosity_converter/questions/numerical.rb
|
125
|
+
- lib/canvas_qti_to_learnosity_converter/questions/question.rb
|
126
|
+
- lib/canvas_qti_to_learnosity_converter/questions/short_answer.rb
|
127
|
+
- lib/canvas_qti_to_learnosity_converter/questions/template_question.rb
|
128
|
+
- lib/canvas_qti_to_learnosity_converter/questions/text_only.rb
|
118
129
|
- lib/canvas_qti_to_learnosity_converter/version.rb
|
119
130
|
homepage: https://github.com/atomicjolt/qti_to_learnosity_converter
|
120
131
|
licenses:
|