canvas_qti_to_learnosity_converter 1.0.0 → 2.1.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/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:
|