ams_migration 0.1.3
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 +7 -0
- data/LICENSE.txt +9 -0
- data/README.md +33 -0
- data/lib/ams_migration/canvas/converter/classic_quiz/calculated.rb +33 -0
- data/lib/ams_migration/canvas/converter/classic_quiz/classic_question.rb +114 -0
- data/lib/ams_migration/canvas/converter/classic_quiz/essay.rb +36 -0
- data/lib/ams_migration/canvas/converter/classic_quiz/file_upload.rb +35 -0
- data/lib/ams_migration/canvas/converter/classic_quiz/fill_in_blank.rb +63 -0
- data/lib/ams_migration/canvas/converter/classic_quiz/fill_in_multiple_blanks.rb +111 -0
- data/lib/ams_migration/canvas/converter/classic_quiz/matching.rb +66 -0
- data/lib/ams_migration/canvas/converter/classic_quiz/multiple_answer.rb +27 -0
- data/lib/ams_migration/canvas/converter/classic_quiz/multiple_choice.rb +57 -0
- data/lib/ams_migration/canvas/converter/classic_quiz/multiple_dropdown.rb +126 -0
- data/lib/ams_migration/canvas/converter/classic_quiz/numerical.rb +89 -0
- data/lib/ams_migration/canvas/converter/classic_quiz/text.rb +28 -0
- data/lib/ams_migration/canvas/converter/classic_quiz/true_false.rb +14 -0
- data/lib/ams_migration/canvas/converter/classic_quiz/unknown.rb +31 -0
- data/lib/ams_migration/canvas/converter/new_quiz/categorization.rb +108 -0
- data/lib/ams_migration/canvas/converter/new_quiz/essay.rb +78 -0
- data/lib/ams_migration/canvas/converter/new_quiz/file_upload.rb +33 -0
- data/lib/ams_migration/canvas/converter/new_quiz/fill_in_blank.rb +180 -0
- data/lib/ams_migration/canvas/converter/new_quiz/formula.rb +33 -0
- data/lib/ams_migration/canvas/converter/new_quiz/hotspot.rb +166 -0
- data/lib/ams_migration/canvas/converter/new_quiz/matching.rb +85 -0
- data/lib/ams_migration/canvas/converter/new_quiz/multiple_answer.rb +28 -0
- data/lib/ams_migration/canvas/converter/new_quiz/multiple_choice.rb +70 -0
- data/lib/ams_migration/canvas/converter/new_quiz/new_question.rb +133 -0
- data/lib/ams_migration/canvas/converter/new_quiz/numerical.rb +117 -0
- data/lib/ams_migration/canvas/converter/new_quiz/ordering.rb +68 -0
- data/lib/ams_migration/canvas/converter/new_quiz/stimulus.rb +35 -0
- data/lib/ams_migration/canvas/converter/new_quiz/true_false.rb +33 -0
- data/lib/ams_migration/canvas/converter/new_quiz/unknown.rb +30 -0
- data/lib/ams_migration/canvas/converter/question_type.rb +30 -0
- data/lib/ams_migration/services/README.md +26 -0
- data/lib/ams_migration/services/data_provider.rb +26 -0
- data/lib/ams_migration/services/learnosity_bank_cleaner.rb +44 -0
- data/lib/ams_migration/services/learnosity_data_api_client.rb +135 -0
- data/lib/ams_migration/services/learnosity_sink.rb +190 -0
- data/lib/ams_migration/services/migration_service.rb +37 -0
- data/lib/ams_migration/services/new_quiz_learnosity_transformer.rb +115 -0
- data/lib/ams_migration/version.rb +3 -0
- data/lib/ams_migration.rb +45 -0
- data/lib/factories/classic_quiz_item_converter_factory.rb +43 -0
- data/lib/factories/new_quiz_item_converter_factory.rb +76 -0
- data/spec/fixtures/sample_data/classic_quiz/calculated.json +24 -0
- data/spec/fixtures/sample_data/classic_quiz/essay.json +24 -0
- data/spec/fixtures/sample_data/classic_quiz/file_upload.json +24 -0
- data/spec/fixtures/sample_data/classic_quiz/fill_in_bank.json +24 -0
- data/spec/fixtures/sample_data/classic_quiz/fill_in_blank.json +53 -0
- data/spec/fixtures/sample_data/classic_quiz/fill_in_multiple_blanks.json +57 -0
- data/spec/fixtures/sample_data/classic_quiz/matching.json +107 -0
- data/spec/fixtures/sample_data/classic_quiz/multiple_answer.json +48 -0
- data/spec/fixtures/sample_data/classic_quiz/multiple_choice.json +56 -0
- data/spec/fixtures/sample_data/classic_quiz/multiple_dropdown.json +73 -0
- data/spec/fixtures/sample_data/classic_quiz/numerical.json +55 -0
- data/spec/fixtures/sample_data/classic_quiz/text.json +24 -0
- data/spec/fixtures/sample_data/classic_quiz/true_false.json +39 -0
- data/spec/fixtures/sample_data/classic_quiz/unknown.json +24 -0
- data/spec/fixtures/sample_data/new_quiz/bank_entry_item.json +5 -0
- data/spec/fixtures/sample_data/new_quiz/bank_item.json +11 -0
- data/spec/fixtures/sample_data/new_quiz/categorization.json +135 -0
- data/spec/fixtures/sample_data/new_quiz/essay.json +40 -0
- data/spec/fixtures/sample_data/new_quiz/file_upload.json +30 -0
- data/spec/fixtures/sample_data/new_quiz/fill_in_blank_association.json +162 -0
- data/spec/fixtures/sample_data/new_quiz/fill_in_blank_dropdown.json +180 -0
- data/spec/fixtures/sample_data/new_quiz/fill_in_blank_math.json +45 -0
- data/spec/fixtures/sample_data/new_quiz/fill_in_blank_mixed.json +188 -0
- data/spec/fixtures/sample_data/new_quiz/fill_in_blank_text.json +140 -0
- data/spec/fixtures/sample_data/new_quiz/formula.json +51 -0
- data/spec/fixtures/sample_data/new_quiz/hotspot_oval.json +38 -0
- data/spec/fixtures/sample_data/new_quiz/hotspot_polygon.json +66 -0
- data/spec/fixtures/sample_data/new_quiz/hotspot_square.json +38 -0
- data/spec/fixtures/sample_data/new_quiz/matching.json +88 -0
- data/spec/fixtures/sample_data/new_quiz/multiple_answer.json +57 -0
- data/spec/fixtures/sample_data/new_quiz/multiple_choice.json +66 -0
- data/spec/fixtures/sample_data/new_quiz/multiple_choice_math.json +55 -0
- data/spec/fixtures/sample_data/new_quiz/numerical.json +64 -0
- data/spec/fixtures/sample_data/new_quiz/ordering.json +60 -0
- data/spec/fixtures/sample_data/new_quiz/stimulus.json +17 -0
- data/spec/fixtures/sample_data/new_quiz/true_false.json +27 -0
- data/spec/fixtures/sample_data/new_quiz/unknown.json +14 -0
- data/spec/lib/ams_migration/services/learnosity_sink_spec.rb +198 -0
- data/spec/lib/canvas/converter/classic_quiz/calculated_spec.rb +26 -0
- data/spec/lib/canvas/converter/classic_quiz/essay_spec.rb +26 -0
- data/spec/lib/canvas/converter/classic_quiz/file_upload_spec.rb +23 -0
- data/spec/lib/canvas/converter/classic_quiz/fill_in_blank_spec.rb +30 -0
- data/spec/lib/canvas/converter/classic_quiz/fill_in_multiple_blanks_spec.rb +33 -0
- data/spec/lib/canvas/converter/classic_quiz/matching_spec.rb +38 -0
- data/spec/lib/canvas/converter/classic_quiz/multiple_answer_spec.rb +33 -0
- data/spec/lib/canvas/converter/classic_quiz/multiple_choice_spec.rb +31 -0
- data/spec/lib/canvas/converter/classic_quiz/multiple_dropdown_spec.rb +39 -0
- data/spec/lib/canvas/converter/classic_quiz/numerical_spec.rb +41 -0
- data/spec/lib/canvas/converter/classic_quiz/text_spec.rb +20 -0
- data/spec/lib/canvas/converter/classic_quiz/true_false_spec.rb +28 -0
- data/spec/lib/canvas/converter/classic_quiz/unknown_spec.rb +25 -0
- data/spec/lib/canvas/converter/common_error.rb +16 -0
- data/spec/lib/canvas/converter/new_quiz/categorization_spec.rb +55 -0
- data/spec/lib/canvas/converter/new_quiz/essay_spec.rb +26 -0
- data/spec/lib/canvas/converter/new_quiz/file_upload_spec.rb +24 -0
- data/spec/lib/canvas/converter/new_quiz/fill_in_blank_spec.rb +111 -0
- data/spec/lib/canvas/converter/new_quiz/formula_spec.rb +23 -0
- data/spec/lib/canvas/converter/new_quiz/hotspot_spec.rb +182 -0
- data/spec/lib/canvas/converter/new_quiz/matching_spec.rb +30 -0
- data/spec/lib/canvas/converter/new_quiz/multiple_answer_spec.rb +34 -0
- data/spec/lib/canvas/converter/new_quiz/multiple_choice_spec.rb +54 -0
- data/spec/lib/canvas/converter/new_quiz/numerical_spec.rb +48 -0
- data/spec/lib/canvas/converter/new_quiz/ordering_spec.rb +25 -0
- data/spec/lib/canvas/converter/new_quiz/stimulus_spec.rb +20 -0
- data/spec/lib/canvas/converter/new_quiz/true_false_spec.rb +24 -0
- data/spec/lib/canvas/converter/new_quiz/unknown_spec.rb +36 -0
- data/spec/lib/factories/classic_quiz_item_converter_factory_spec.rb +183 -0
- data/spec/lib/factories/new_quiz_item_converter_factory_spec.rb +71 -0
- data/spec/lib/services/learnosity_data_api_client_spec.rb +130 -0
- data/spec/lib/services/new_quiz_learnosity_transformer_spec.rb +196 -0
- data/spec/spec_helper.rb +20 -0
- metadata +229 -0
@@ -0,0 +1,33 @@
|
|
1
|
+
# Copyright (C) 2025 - present Instructure, Inc.
|
2
|
+
# This code is made publicly available for educational and reference purposes only.
|
3
|
+
# You may not copy, modify, redistribute, or use this code in any software or service without explicit permission.
|
4
|
+
|
5
|
+
module AmsMigration
|
6
|
+
module Canvas
|
7
|
+
module Converter
|
8
|
+
module NewQuiz
|
9
|
+
class Formula < NewQuestion
|
10
|
+
# This question type will be skipped in the conversion process and the
|
11
|
+
# question will not be included in the quiz. The JSON returned will be
|
12
|
+
# an essay question with text noting the question has been skipped.
|
13
|
+
def convert
|
14
|
+
super
|
15
|
+
@converted_question = {
|
16
|
+
stimulus: "The Formula item was removed from the quiz because there is no equivalent item type in Mastery Connect. Please check the original quiz for this question.",
|
17
|
+
type: QuestionType::UNSUPPORTED_CANVAS_TYPE,
|
18
|
+
validation: {
|
19
|
+
valid_response: {
|
20
|
+
score: 0,
|
21
|
+
value: ""
|
22
|
+
},
|
23
|
+
scoring_type: "exactMatch"
|
24
|
+
},
|
25
|
+
is_math: true
|
26
|
+
}
|
27
|
+
format
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
# Copyright (C) 2025 - present Instructure, Inc.
|
2
|
+
# This code is made publicly available for educational and reference purposes only.
|
3
|
+
# You may not copy, modify, redistribute, or use this code in any software or service without explicit permission.
|
4
|
+
|
5
|
+
require "fastimage"
|
6
|
+
|
7
|
+
module AmsMigration
|
8
|
+
module Canvas
|
9
|
+
module Converter
|
10
|
+
module NewQuiz
|
11
|
+
class Hotspot < NewQuestion
|
12
|
+
DEFAULT_IMAGE_SIZE = [500, 500].freeze
|
13
|
+
AREA_ATTRIBUTES = {
|
14
|
+
global: {
|
15
|
+
fill: "rgba(12, 176, 216, 0.48)",
|
16
|
+
stroke: "rgba(25, 90, 107, 0.83)"
|
17
|
+
}
|
18
|
+
}.freeze
|
19
|
+
|
20
|
+
def to_h
|
21
|
+
@question
|
22
|
+
end
|
23
|
+
|
24
|
+
def image_url
|
25
|
+
@question&.dig(:entry, :interaction_data, :image_url)
|
26
|
+
end
|
27
|
+
|
28
|
+
def image_dimensions
|
29
|
+
return @image_dimensions if defined? @image_dimensions
|
30
|
+
|
31
|
+
dimensions = FastImage.size(image_url)
|
32
|
+
@image_dimensions = dimensions.nil? ? DEFAULT_IMAGE_SIZE : dimensions
|
33
|
+
|
34
|
+
@image_dimensions
|
35
|
+
end
|
36
|
+
|
37
|
+
def points_possible
|
38
|
+
@question[:points_possible]
|
39
|
+
end
|
40
|
+
|
41
|
+
# We multiply by 100 because the canvas coordinates are a percentage of the image size whereas the
|
42
|
+
# MC coordinates are a full integer percentage of the image size
|
43
|
+
def canvas_coords(value)
|
44
|
+
value[:coordinates].map do |coord|
|
45
|
+
{
|
46
|
+
x: coord[:x] * 100,
|
47
|
+
y: coord[:y] * 100
|
48
|
+
}
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def values
|
53
|
+
value = @question.dig(:entry, :scoring_data, :value)
|
54
|
+
return value if value.is_a?(Array)
|
55
|
+
|
56
|
+
[value]
|
57
|
+
end
|
58
|
+
|
59
|
+
def coordinates(value)
|
60
|
+
type = value[:type]
|
61
|
+
|
62
|
+
case type
|
63
|
+
when "square"
|
64
|
+
square_coordinates(value)
|
65
|
+
when "oval"
|
66
|
+
oval_coordinates(value)
|
67
|
+
when "polygon"
|
68
|
+
canvas_coords(value)
|
69
|
+
else
|
70
|
+
raise "Unknown hotspot type: #{type}"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def square_coordinates(value)
|
75
|
+
canvas_coordinates = canvas_coords(value)
|
76
|
+
|
77
|
+
x1 = canvas_coordinates[0][:x]
|
78
|
+
y1 = canvas_coordinates[0][:y]
|
79
|
+
x2 = canvas_coordinates[1][:x]
|
80
|
+
y2 = canvas_coordinates[1][:y]
|
81
|
+
|
82
|
+
[
|
83
|
+
{
|
84
|
+
x: x1,
|
85
|
+
y: y1
|
86
|
+
},
|
87
|
+
{
|
88
|
+
x: x1,
|
89
|
+
y: y2
|
90
|
+
},
|
91
|
+
{
|
92
|
+
x: x2,
|
93
|
+
y: y2
|
94
|
+
},
|
95
|
+
{
|
96
|
+
x: x2,
|
97
|
+
y: y1
|
98
|
+
}
|
99
|
+
]
|
100
|
+
end
|
101
|
+
|
102
|
+
def oval_coordinates(value)
|
103
|
+
canvas_coordinates = canvas_coords(value)
|
104
|
+
x1 = canvas_coordinates[0][:x]
|
105
|
+
y1 = canvas_coordinates[0][:y]
|
106
|
+
x2 = canvas_coordinates[1][:x]
|
107
|
+
y2 = canvas_coordinates[1][:y]
|
108
|
+
|
109
|
+
oval_center = [(x1 + x2) / 2, (y1 + y2) / 2]
|
110
|
+
horizontal_radius = (x2 - x1) / 2
|
111
|
+
vertical_radius = (y2 - y1) / 2
|
112
|
+
|
113
|
+
# Going to approximate the oval with a polygon with 16 points
|
114
|
+
oval_coords = []
|
115
|
+
|
116
|
+
16.times do |i|
|
117
|
+
angle = (2 * Math::PI) * (i / 16.0)
|
118
|
+
x = oval_center[0] + (horizontal_radius * Math.cos(angle))
|
119
|
+
y = oval_center[1] + (vertical_radius * Math.sin(angle))
|
120
|
+
oval_coords << { x:, y: }
|
121
|
+
end
|
122
|
+
|
123
|
+
oval_coords
|
124
|
+
end
|
125
|
+
|
126
|
+
def areas
|
127
|
+
values.map { |value| coordinates(value) }
|
128
|
+
end
|
129
|
+
|
130
|
+
def convert
|
131
|
+
super
|
132
|
+
begin
|
133
|
+
@converted_question = {
|
134
|
+
type: "hotspot",
|
135
|
+
stimulus:,
|
136
|
+
image: {
|
137
|
+
source: image_url,
|
138
|
+
width: image_dimensions[0],
|
139
|
+
height: image_dimensions[1]
|
140
|
+
},
|
141
|
+
areas:,
|
142
|
+
area_attributes: AREA_ATTRIBUTES,
|
143
|
+
validation: {
|
144
|
+
scoring_type: "exactMatch",
|
145
|
+
valid_response: {
|
146
|
+
score: points_possible,
|
147
|
+
value: ["0"]
|
148
|
+
}
|
149
|
+
},
|
150
|
+
is_math: true
|
151
|
+
}
|
152
|
+
|
153
|
+
@image_sources << image_url
|
154
|
+
|
155
|
+
@notifications << "Hotspot items will need additional configuration."
|
156
|
+
rescue => e
|
157
|
+
@conversion_errors << e.message
|
158
|
+
end
|
159
|
+
post_convert
|
160
|
+
format
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# Copyright (C) 2025 - present Instructure, Inc.
|
2
|
+
# This code is made publicly available for educational and reference purposes only.
|
3
|
+
# You may not copy, modify, redistribute, or use this code in any software or service without explicit permission.
|
4
|
+
|
5
|
+
module AmsMigration
|
6
|
+
module Canvas
|
7
|
+
module Converter
|
8
|
+
module NewQuiz
|
9
|
+
class Matching < NewQuestion
|
10
|
+
def initialize(question)
|
11
|
+
super
|
12
|
+
@distractors = []
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_h
|
16
|
+
@question
|
17
|
+
end
|
18
|
+
|
19
|
+
def possible_responses
|
20
|
+
@question.dig(:entry, :interaction_data, :answers).map do |answer|
|
21
|
+
answer
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def stimulus_list
|
26
|
+
@question.dig(:entry, :interaction_data, :questions).map do |question|
|
27
|
+
question[:item_body]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def answer_value
|
32
|
+
@question.dig(:entry, :scoring_data, :edit_data, :matches).map do |answer|
|
33
|
+
answer[:answer_body]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def scoring_type
|
38
|
+
case @question.dig(:entry, :scoring_algorithm)
|
39
|
+
when "PartialDeep"
|
40
|
+
"partialMatchV2"
|
41
|
+
else
|
42
|
+
"exactMatch"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def distractors
|
47
|
+
@distractors = @question.dig(:entry, :scoring_data, :edit_data, :distractors)
|
48
|
+
end
|
49
|
+
|
50
|
+
def convert
|
51
|
+
super
|
52
|
+
begin
|
53
|
+
@converted_question = {
|
54
|
+
possible_responses:,
|
55
|
+
stimulus_list:,
|
56
|
+
stimulus:,
|
57
|
+
type: "association",
|
58
|
+
validation: {
|
59
|
+
valid_response: {
|
60
|
+
score: point_value,
|
61
|
+
value: answer_value
|
62
|
+
},
|
63
|
+
scoring_type:
|
64
|
+
},
|
65
|
+
metadata: {
|
66
|
+
distractor_rationale_response_level: distractors
|
67
|
+
}
|
68
|
+
}
|
69
|
+
|
70
|
+
if distractors.length.positive?
|
71
|
+
@converted_question[:metadata] = {
|
72
|
+
distractor_rationale_response_level: @distractors
|
73
|
+
}
|
74
|
+
end
|
75
|
+
rescue => e
|
76
|
+
@conversion_errors << e.message
|
77
|
+
end
|
78
|
+
post_convert
|
79
|
+
format
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# Copyright (C) 2025 - present Instructure, Inc.
|
2
|
+
# This code is made publicly available for educational and reference purposes only.
|
3
|
+
# You may not copy, modify, redistribute, or use this code in any software or service without explicit permission.
|
4
|
+
|
5
|
+
module AmsMigration
|
6
|
+
module Canvas
|
7
|
+
module Converter
|
8
|
+
module NewQuiz
|
9
|
+
class MultipleAnswer < MultipleChoice
|
10
|
+
def correct_answer
|
11
|
+
@question[:entry][:scoring_data][:value]
|
12
|
+
end
|
13
|
+
|
14
|
+
def convert
|
15
|
+
begin
|
16
|
+
multiple_answer = super
|
17
|
+
multiple_answer[:question][:multiple_responses] = true if multiple_answer[:errors].empty?
|
18
|
+
rescue => e
|
19
|
+
@conversion_errors << e.message
|
20
|
+
end
|
21
|
+
post_convert
|
22
|
+
format
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# Copyright (C) 2025 - present Instructure, Inc.
|
2
|
+
# This code is made publicly available for educational and reference purposes only.
|
3
|
+
# You may not copy, modify, redistribute, or use this code in any software or service without explicit permission.
|
4
|
+
|
5
|
+
module AmsMigration
|
6
|
+
module Canvas
|
7
|
+
module Converter
|
8
|
+
module NewQuiz
|
9
|
+
class MultipleChoice < NewQuestion
|
10
|
+
def initialize(question)
|
11
|
+
super
|
12
|
+
@distractors = []
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_h
|
16
|
+
@question
|
17
|
+
end
|
18
|
+
|
19
|
+
def options
|
20
|
+
options = []
|
21
|
+
@question[:entry][:interaction_data][:choices].each do |answer|
|
22
|
+
label = answer[:item_body]
|
23
|
+
options << {
|
24
|
+
label:,
|
25
|
+
value: answer[:id].to_s
|
26
|
+
}
|
27
|
+
unless @question[:entry][:answer_feedback][answer[:id].to_sym].nil?
|
28
|
+
@distractors << @question[:entry][:answer_feedback][answer[:id].to_sym]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
options
|
32
|
+
end
|
33
|
+
|
34
|
+
def correct_answer
|
35
|
+
[@question[:entry][:scoring_data][:value]]
|
36
|
+
end
|
37
|
+
|
38
|
+
def convert
|
39
|
+
super
|
40
|
+
begin
|
41
|
+
@converted_question = {
|
42
|
+
stimulus:,
|
43
|
+
options:,
|
44
|
+
type: "mcq",
|
45
|
+
validation: {
|
46
|
+
valid_response: {
|
47
|
+
score: point_value,
|
48
|
+
value: correct_answer
|
49
|
+
},
|
50
|
+
scoring_type: "exactMatch"
|
51
|
+
}
|
52
|
+
}
|
53
|
+
|
54
|
+
if @distractors.length.positive?
|
55
|
+
@converted_question[:metadata] = {
|
56
|
+
distractor_rationale_response_level: @distractors
|
57
|
+
}
|
58
|
+
end
|
59
|
+
rescue => e
|
60
|
+
@conversion_errors << e.message
|
61
|
+
end
|
62
|
+
|
63
|
+
post_convert
|
64
|
+
format
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# Copyright (C) 2025 - present Instructure, Inc.
|
2
|
+
# This code is made publicly available for educational and reference purposes only.
|
3
|
+
# You may not copy, modify, redistribute, or use this code in any software or service without explicit permission.
|
4
|
+
|
5
|
+
module AmsMigration
|
6
|
+
require "nokogiri"
|
7
|
+
module Canvas
|
8
|
+
module Converter
|
9
|
+
module NewQuiz
|
10
|
+
class NewQuestion
|
11
|
+
attr_reader :notifications, :image_sources
|
12
|
+
|
13
|
+
def initialize(question)
|
14
|
+
@question_json = question
|
15
|
+
@question = nil
|
16
|
+
@is_math = nil
|
17
|
+
@converted_question = {}
|
18
|
+
@conversion_errors = []
|
19
|
+
@notifications = []
|
20
|
+
@image_sources = []
|
21
|
+
@uri_parser = URI::DEFAULT_PARSER
|
22
|
+
end
|
23
|
+
|
24
|
+
def title
|
25
|
+
return nil if @question.blank?
|
26
|
+
|
27
|
+
@question[:question_name] || @question[:title] || @question.dig(:entry, :title) || ""
|
28
|
+
end
|
29
|
+
|
30
|
+
def stimulus
|
31
|
+
@question[:entry][:item_body]
|
32
|
+
end
|
33
|
+
|
34
|
+
def convert
|
35
|
+
begin
|
36
|
+
@question = JSON.parse(@question_json).deep_symbolize_keys
|
37
|
+
rescue JSON::ParserError => e
|
38
|
+
@question = ""
|
39
|
+
@conversion_errors << e.message
|
40
|
+
end
|
41
|
+
format
|
42
|
+
end
|
43
|
+
|
44
|
+
def point_value
|
45
|
+
point_value = @question[:points_possible] || 0
|
46
|
+
if point_value % 1 != 0
|
47
|
+
@notifications << "Partial points are not supported. After conversion points have been rounded to the nearest whole number."
|
48
|
+
end
|
49
|
+
point_value.round(0)
|
50
|
+
end
|
51
|
+
|
52
|
+
def post_convert
|
53
|
+
process_question_text
|
54
|
+
end
|
55
|
+
|
56
|
+
def process_question_text
|
57
|
+
if @converted_question.is_a?(Hash)
|
58
|
+
if @converted_question.key?(:stimulus)
|
59
|
+
@converted_question[:stimulus] = substitute_mathml(@converted_question[:stimulus])
|
60
|
+
get_image_sources(@converted_question[:stimulus])
|
61
|
+
end
|
62
|
+
if @converted_question.key?(:template)
|
63
|
+
@converted_question[:template] = substitute_mathml(@converted_question[:template])
|
64
|
+
get_image_sources(@converted_question[:template])
|
65
|
+
end
|
66
|
+
|
67
|
+
if @converted_question.key?(:options)
|
68
|
+
@converted_question[:options].each do |option|
|
69
|
+
option[:label] = substitute_mathml(option[:label])
|
70
|
+
get_image_sources(option[:label])
|
71
|
+
option[:value] = substitute_mathml(option[:value])
|
72
|
+
get_image_sources(option[:value])
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
if @converted_question.key?(:list)
|
77
|
+
@converted_question[:list].each_with_index do |each_list, index|
|
78
|
+
@converted_question[:list][index] = substitute_mathml(each_list)
|
79
|
+
get_image_sources(@converted_question[:list][index])
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
@converted_question[:is_math] = @converted_question[:is_math] || @is_math
|
84
|
+
@converted_question.compact!
|
85
|
+
else
|
86
|
+
@converted_question = substitute_mathml(@converted_question) unless @converted_question.nil?
|
87
|
+
get_image_sources(@converted_question) unless @converted_question.nil?
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def substitute_mathml(fragment)
|
92
|
+
html = Nokogiri::HTML::DocumentFragment.parse(fragment, "UTF-8")
|
93
|
+
|
94
|
+
if @is_math.nil?
|
95
|
+
@is_math = html.css("img.equation_image").empty? ? nil : true
|
96
|
+
end
|
97
|
+
|
98
|
+
html.css("img.equation_image[data-equation-content]").each do |img|
|
99
|
+
img.replace("\\(#{img.attr("data-equation-content")}\\)")
|
100
|
+
end
|
101
|
+
|
102
|
+
html.to_xhtml(indent: 0).delete("\n")
|
103
|
+
end
|
104
|
+
|
105
|
+
def get_image_sources(fragment)
|
106
|
+
html = Nokogiri::HTML::DocumentFragment.parse(fragment, "UTF-8")
|
107
|
+
html.css("img").each { |img| @image_sources << img.attr("src") if img.attr("src") }
|
108
|
+
end
|
109
|
+
|
110
|
+
def tags
|
111
|
+
@question.is_a?(Hash) ? @question[:tags] || [] : []
|
112
|
+
end
|
113
|
+
|
114
|
+
def to_h
|
115
|
+
@question
|
116
|
+
end
|
117
|
+
|
118
|
+
protected
|
119
|
+
|
120
|
+
def format
|
121
|
+
{
|
122
|
+
question: @converted_question,
|
123
|
+
title:,
|
124
|
+
errors: @conversion_errors,
|
125
|
+
tags:,
|
126
|
+
image_sources:
|
127
|
+
}
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# Copyright (C) 2025 - present Instructure, Inc.
|
2
|
+
# This code is made publicly available for educational and reference purposes only.
|
3
|
+
# You may not copy, modify, redistribute, or use this code in any software or service without explicit permission.
|
4
|
+
|
5
|
+
module AmsMigration
|
6
|
+
module Canvas
|
7
|
+
module Converter
|
8
|
+
module NewQuiz
|
9
|
+
class Numerical < NewQuestion
|
10
|
+
def math?
|
11
|
+
@question[:entry][:scoring_data][:value].any? do |value|
|
12
|
+
%w[withinARange marginOfError].include?(value[:type])
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def correct_values
|
17
|
+
values = []
|
18
|
+
@question[:entry][:scoring_data][:value].map do |answer|
|
19
|
+
case answer[:type]
|
20
|
+
when "exactResponse"
|
21
|
+
values << {
|
22
|
+
method: "equivLiteral",
|
23
|
+
value: answer[:value],
|
24
|
+
options: {
|
25
|
+
ignoreOrder: false,
|
26
|
+
inverseResult: false
|
27
|
+
}
|
28
|
+
}
|
29
|
+
when "marginOfError"
|
30
|
+
@notifications << 'Numeric items with "Margin Of Error" may need additional configuration'
|
31
|
+
|
32
|
+
value = answer[:value].to_f
|
33
|
+
margin = answer[:margin].to_f
|
34
|
+
|
35
|
+
margin = (value * margin / 100).round(2) if answer[:margin_type] == "percent"
|
36
|
+
|
37
|
+
values << {
|
38
|
+
method: "equivValue",
|
39
|
+
value: "#{value}\\pm#{margin}",
|
40
|
+
options: {
|
41
|
+
ignoreOrder: false,
|
42
|
+
inverseResult: false
|
43
|
+
}
|
44
|
+
}
|
45
|
+
when "withinARange"
|
46
|
+
@notifications << 'Numeric items with "Within A Range" may need additional configuration'
|
47
|
+
if answer[:end] && answer[:start]
|
48
|
+
mid_point = (answer[:end].to_f + answer[:start].to_f) / 2
|
49
|
+
margin_of_error = (answer[:end].to_f - answer[:start].to_f) / 2
|
50
|
+
values << {
|
51
|
+
method: "equivValue",
|
52
|
+
value: "#{mid_point}\\pm#{margin_of_error}",
|
53
|
+
options: {
|
54
|
+
ignoreOrder: false,
|
55
|
+
inverseResult: false
|
56
|
+
}
|
57
|
+
}
|
58
|
+
end
|
59
|
+
when "preciseResponse"
|
60
|
+
@notifications << 'Numeric items with "Precise Response" may need additional configuration'
|
61
|
+
|
62
|
+
value = answer[:value].to_f
|
63
|
+
precision = answer[:precision].to_i
|
64
|
+
|
65
|
+
if answer[:precision_type] == "significantDigits"
|
66
|
+
whole_num_digits = value.to_i.to_s.length
|
67
|
+
decimal_places = [precision - whole_num_digits, 0].max
|
68
|
+
else
|
69
|
+
decimal_places = precision
|
70
|
+
end
|
71
|
+
|
72
|
+
values << {
|
73
|
+
method: "equivValue",
|
74
|
+
value: answer[:value].to_s,
|
75
|
+
options: {
|
76
|
+
decimalPlaces: decimal_places
|
77
|
+
}
|
78
|
+
}
|
79
|
+
else
|
80
|
+
raise JSON::ParserError, "Invalid scoring type for Numeric"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
values
|
84
|
+
end
|
85
|
+
|
86
|
+
def convert
|
87
|
+
super
|
88
|
+
begin
|
89
|
+
@converted_question = {
|
90
|
+
stimulus:,
|
91
|
+
template: "{{response}}",
|
92
|
+
response_container: {
|
93
|
+
template: ""
|
94
|
+
},
|
95
|
+
type: "clozeformula",
|
96
|
+
validation: {
|
97
|
+
scoring_type: "exactMatch",
|
98
|
+
valid_response: {
|
99
|
+
score: point_value,
|
100
|
+
value: [correct_values]
|
101
|
+
}
|
102
|
+
},
|
103
|
+
response_containers: []
|
104
|
+
}
|
105
|
+
|
106
|
+
@converted_question[:is_math] = true if math?
|
107
|
+
rescue => e
|
108
|
+
@conversion_errors << e.message
|
109
|
+
end
|
110
|
+
post_convert
|
111
|
+
format
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# Copyright (C) 2025 - present Instructure, Inc.
|
2
|
+
# This code is made publicly available for educational and reference purposes only.
|
3
|
+
# You may not copy, modify, redistribute, or use this code in any software or service without explicit permission.
|
4
|
+
|
5
|
+
module AmsMigration
|
6
|
+
module Canvas
|
7
|
+
module Converter
|
8
|
+
module NewQuiz
|
9
|
+
class Ordering < NewQuestion
|
10
|
+
def to_h
|
11
|
+
@question
|
12
|
+
end
|
13
|
+
|
14
|
+
def list
|
15
|
+
list = []
|
16
|
+
# already shuffled list (fixed list, not random) coming from API
|
17
|
+
@question[:entry][:interaction_data][:choices].each_value do |answer|
|
18
|
+
list << answer[:item_body]
|
19
|
+
end
|
20
|
+
list
|
21
|
+
end
|
22
|
+
|
23
|
+
def correct_answer
|
24
|
+
if !@question[:entry][:properties][:top_label].empty? || !@question[:entry][:properties][:bottom_label].empty?
|
25
|
+
@notifications << "Please review this item. Top and bottom labels on Ordering questions are not supported."
|
26
|
+
end
|
27
|
+
answers = @question[:entry][:scoring_data][:value]
|
28
|
+
choices = @question[:entry][:interaction_data][:choices]
|
29
|
+
answers.map { |id| choices.keys.index(:"#{id}") }
|
30
|
+
end
|
31
|
+
|
32
|
+
def scoring_type
|
33
|
+
"exactMatch"
|
34
|
+
end
|
35
|
+
|
36
|
+
def ui_style
|
37
|
+
{
|
38
|
+
type: @question[:entry][:properties][:display_answers_paragraph] ? "inline" : "list"
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
def convert
|
43
|
+
super
|
44
|
+
begin
|
45
|
+
@converted_question = {
|
46
|
+
list:,
|
47
|
+
stimulus:,
|
48
|
+
type: "orderlist",
|
49
|
+
validation: {
|
50
|
+
scoring_type:,
|
51
|
+
valid_response: {
|
52
|
+
score: point_value,
|
53
|
+
value: correct_answer
|
54
|
+
}
|
55
|
+
},
|
56
|
+
ui_style:
|
57
|
+
}
|
58
|
+
rescue => e
|
59
|
+
@conversion_errors << e.message
|
60
|
+
end
|
61
|
+
post_convert
|
62
|
+
format
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|