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,126 @@
|
|
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 ClassicQuiz
|
9
|
+
class MultipleDropdown < ClassicQuestion
|
10
|
+
def correct_answer
|
11
|
+
valid_values = {}
|
12
|
+
alternate_values = {}
|
13
|
+
possible_values = {}
|
14
|
+
ordered_templates = []
|
15
|
+
stimulus.scan(/\[.*?\]/).each_with_index do |template, _index|
|
16
|
+
template = template.delete("[").delete("]")
|
17
|
+
ordered_templates << template
|
18
|
+
@question[:answers].select { |answer| answer[:blank_id] == template }.each_with_index do |answer, index|
|
19
|
+
if answer[:text] && answer[:blank_id]
|
20
|
+
if possible_values[answer[:blank_id]]
|
21
|
+
possible_values[answer[:blank_id]] << answer[:text]
|
22
|
+
else
|
23
|
+
possible_values[answer[:blank_id]] = [answer[:text]]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
if index.zero? && answer[:text] && answer[:blank_id]
|
27
|
+
if valid_values[answer[:blank_id]]
|
28
|
+
valid_values[answer[:blank_id]] << answer[:text]
|
29
|
+
else
|
30
|
+
valid_values[answer[:blank_id]] = [answer[:text]]
|
31
|
+
end
|
32
|
+
elsif answer[:text] && answer[:blank_id]
|
33
|
+
if alternate_values[answer[:blank_id]]
|
34
|
+
alternate_values[answer[:blank_id]] << answer[:text]
|
35
|
+
else
|
36
|
+
alternate_values[answer[:blank_id]] = [answer[:text]]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
valid_values.values.map(&:size).max
|
43
|
+
|
44
|
+
correct_values = ordered_templates.map do |template|
|
45
|
+
valid_values[template].first
|
46
|
+
end
|
47
|
+
|
48
|
+
valid_response = {
|
49
|
+
score: point_value,
|
50
|
+
value: correct_values
|
51
|
+
}
|
52
|
+
|
53
|
+
alternate_responses = []
|
54
|
+
max_answers = alternate_values.values.map(&:size).max
|
55
|
+
(0..(max_answers - 1)).each do |i|
|
56
|
+
alternate_response = ordered_templates.filter_map do |template|
|
57
|
+
alternate_values&.dig(template, i)
|
58
|
+
end
|
59
|
+
alternate_responses << {
|
60
|
+
score: 1,
|
61
|
+
value: alternate_response
|
62
|
+
}
|
63
|
+
end
|
64
|
+
|
65
|
+
possible_responses = ordered_templates.map do |template|
|
66
|
+
possible_values[template]
|
67
|
+
end
|
68
|
+
{
|
69
|
+
valid_response:,
|
70
|
+
alternate_responses:,
|
71
|
+
possible_responses:
|
72
|
+
}
|
73
|
+
end
|
74
|
+
|
75
|
+
def distractors
|
76
|
+
results = []
|
77
|
+
has_distractors = false
|
78
|
+
@question[:answers].each do |answer|
|
79
|
+
distractor = answer[:comments_html] ||= ""
|
80
|
+
results << distractor
|
81
|
+
has_distractors = true if distractor != ""
|
82
|
+
end
|
83
|
+
has_distractors ? results : nil
|
84
|
+
end
|
85
|
+
|
86
|
+
def convert
|
87
|
+
super
|
88
|
+
begin
|
89
|
+
correct_values = correct_answer
|
90
|
+
|
91
|
+
@converted_question = {
|
92
|
+
template: stimulus.gsub("[", "{{response}}").delete("]").to_s,
|
93
|
+
type: "clozedropdown",
|
94
|
+
possible_responses: correct_values[:possible_responses],
|
95
|
+
validation: {
|
96
|
+
valid_response: correct_values[:valid_response],
|
97
|
+
scoring_type: "exactMatch",
|
98
|
+
match_all_possible_responses: true
|
99
|
+
},
|
100
|
+
is_math: true,
|
101
|
+
response_container: {
|
102
|
+
pointer: "left"
|
103
|
+
}
|
104
|
+
}
|
105
|
+
|
106
|
+
if correct_values[:alternate_responses].size.positive?
|
107
|
+
@converted_question[:validation][:alt_responses] = correct_values[:alternate_responses]
|
108
|
+
end
|
109
|
+
|
110
|
+
@converted_question[:metadata] = {
|
111
|
+
distractor_rationale_response_level: distractors
|
112
|
+
}
|
113
|
+
rescue => e
|
114
|
+
@conversion_errors << e.message
|
115
|
+
end
|
116
|
+
post_convert
|
117
|
+
{
|
118
|
+
question: @converted_question,
|
119
|
+
errors: @conversion_errors
|
120
|
+
}
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,89 @@
|
|
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 ClassicQuiz
|
9
|
+
class Numerical < ClassicQuestion
|
10
|
+
def correct_values
|
11
|
+
values = []
|
12
|
+
@question[:answers].map do |answer|
|
13
|
+
case answer[:numerical_answer_type]
|
14
|
+
when "exact_answer"
|
15
|
+
values << if answer[:margin].zero?
|
16
|
+
{
|
17
|
+
method: "equivLiteral",
|
18
|
+
value: answer[:exact].to_s,
|
19
|
+
options: {
|
20
|
+
ignoreOrder: false,
|
21
|
+
inverseResult: false
|
22
|
+
}
|
23
|
+
}
|
24
|
+
else
|
25
|
+
{
|
26
|
+
method: "equivValue",
|
27
|
+
value: "#{answer[:exact]}\\pm#{answer[:margin]}"
|
28
|
+
}
|
29
|
+
end
|
30
|
+
when "range_answer"
|
31
|
+
@notifications << 'Numeric items with "Answer in Range" may need additional configuration'
|
32
|
+
if answer[:end] && answer[:start]
|
33
|
+
values << {
|
34
|
+
method: "equivValue",
|
35
|
+
value: "#{answer[:end] - answer[:start]}\\pm#{(answer[:end] - answer[:start]) / 2}",
|
36
|
+
options: {
|
37
|
+
ignoreOrder: false,
|
38
|
+
inverseResult: false
|
39
|
+
}
|
40
|
+
}
|
41
|
+
end
|
42
|
+
when "precision_answer"
|
43
|
+
@notifications << 'Numeric items with "Answer with Precision" may need additional configuration'
|
44
|
+
values << {
|
45
|
+
method: "equivValue",
|
46
|
+
value: answer[:approximate].to_s,
|
47
|
+
options: {
|
48
|
+
decimalPlaces: answer[:precision]
|
49
|
+
}
|
50
|
+
}
|
51
|
+
end
|
52
|
+
end
|
53
|
+
values
|
54
|
+
end
|
55
|
+
|
56
|
+
def convert
|
57
|
+
super
|
58
|
+
begin
|
59
|
+
@converted_question = {
|
60
|
+
stimulus:,
|
61
|
+
template: "{{response}}",
|
62
|
+
response_container: {
|
63
|
+
template: ""
|
64
|
+
},
|
65
|
+
type: "clozeformula",
|
66
|
+
validation: {
|
67
|
+
scoring_type: "exactMatch",
|
68
|
+
valid_response: {
|
69
|
+
score: point_value,
|
70
|
+
value: [correct_values]
|
71
|
+
}
|
72
|
+
},
|
73
|
+
is_math: true,
|
74
|
+
response_containers: []
|
75
|
+
}
|
76
|
+
rescue => e
|
77
|
+
@conversion_errors << e.message
|
78
|
+
end
|
79
|
+
post_convert
|
80
|
+
{
|
81
|
+
question: @converted_question,
|
82
|
+
errors: @conversion_errors
|
83
|
+
}
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
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 ClassicQuiz
|
9
|
+
class Text < ClassicQuestion
|
10
|
+
# This question type only returns the stimulus as the question text
|
11
|
+
def convert
|
12
|
+
super
|
13
|
+
begin
|
14
|
+
@converted_question = stimulus
|
15
|
+
rescue => e
|
16
|
+
@conversion_errors << e.message
|
17
|
+
end
|
18
|
+
post_convert
|
19
|
+
{
|
20
|
+
question: @converted_question,
|
21
|
+
errors: @conversion_errors
|
22
|
+
}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,14 @@
|
|
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 ClassicQuiz
|
9
|
+
class TrueFalse < MultipleChoice
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,31 @@
|
|
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 ClassicQuiz
|
9
|
+
# Implementation for when Canvas QuizItem question_type is unknown.
|
10
|
+
# Allows the Quiz conversion to continue uninterrupted. See MCE-23091.
|
11
|
+
class Unknown < ClassicQuestion
|
12
|
+
# This question type will be skipped in the conversion process and the
|
13
|
+
# question will not be included in the quiz.
|
14
|
+
def convert
|
15
|
+
begin
|
16
|
+
@question = JSON.parse(@question_json).deep_symbolize_keys
|
17
|
+
@conversion_errors << "Unknown question type: #{@question[:question_type]}"
|
18
|
+
rescue JSON::ParserError => e
|
19
|
+
@question = ""
|
20
|
+
@conversion_errors << e.message
|
21
|
+
end
|
22
|
+
{
|
23
|
+
question: "",
|
24
|
+
errors: @conversion_errors
|
25
|
+
}
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,108 @@
|
|
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
|
+
# This class converts 'Categorization' questions from Canvas New Quizzes to the
|
10
|
+
# MasteryConnect 'Classification' question type.
|
11
|
+
class Categorization < NewQuestion
|
12
|
+
def initialize(question)
|
13
|
+
super
|
14
|
+
@distractor_lookup = []
|
15
|
+
@category_lookup = []
|
16
|
+
end
|
17
|
+
|
18
|
+
def convert
|
19
|
+
super
|
20
|
+
begin
|
21
|
+
@converted_question = converted_question
|
22
|
+
rescue => e
|
23
|
+
@conversion_errors << e.message
|
24
|
+
end
|
25
|
+
post_convert
|
26
|
+
format
|
27
|
+
end
|
28
|
+
|
29
|
+
def converted_question
|
30
|
+
@distractor_lookup = build_lookup(@question[:entry][:interaction_data][:distractors])
|
31
|
+
@category_lookup = build_lookup(@question[:entry][:interaction_data][:categories])
|
32
|
+
{
|
33
|
+
possible_responses:,
|
34
|
+
stimulus:,
|
35
|
+
type: "classification",
|
36
|
+
ui_style:,
|
37
|
+
validation:
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
def possible_responses
|
42
|
+
@distractor_lookup.map do |distractor|
|
43
|
+
distractor[:item_body]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def stimulus
|
48
|
+
@question[:entry][:item_body]
|
49
|
+
end
|
50
|
+
|
51
|
+
def ui_style
|
52
|
+
{
|
53
|
+
column_count:,
|
54
|
+
column_titles:
|
55
|
+
}
|
56
|
+
end
|
57
|
+
|
58
|
+
def column_count
|
59
|
+
@question[:entry][:interaction_data][:categories].length
|
60
|
+
end
|
61
|
+
|
62
|
+
def column_titles
|
63
|
+
@question[:entry][:interaction_data][:category_order].map do |id|
|
64
|
+
find_element(@category_lookup, id)[:item_body]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def validation
|
69
|
+
{
|
70
|
+
scoring_type: "exactMatch",
|
71
|
+
valid_response: {
|
72
|
+
score:,
|
73
|
+
value:
|
74
|
+
}
|
75
|
+
}
|
76
|
+
end
|
77
|
+
|
78
|
+
def score
|
79
|
+
@question[:points_possible].round
|
80
|
+
end
|
81
|
+
|
82
|
+
def value
|
83
|
+
@question[:entry][:interaction_data][:category_order].map do |category|
|
84
|
+
category = find_element(@question[:entry][:scoring_data][:value], category)
|
85
|
+
category[:scoring_data][:value].map do |value|
|
86
|
+
find_element(@distractor_lookup, value)[:index]
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def build_lookup(json_data)
|
92
|
+
json_data.map.with_index do |(id, item), index|
|
93
|
+
{
|
94
|
+
id:,
|
95
|
+
item_body: item[:item_body],
|
96
|
+
index:
|
97
|
+
}
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def find_element(collection, id)
|
102
|
+
collection.find { |item| item[:id].to_s == id }
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,78 @@
|
|
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 Essay < NewQuestion
|
10
|
+
def to_h
|
11
|
+
@question
|
12
|
+
end
|
13
|
+
|
14
|
+
def spellcheck
|
15
|
+
@question.dig(:entry, :interaction_data, :spell_check)
|
16
|
+
end
|
17
|
+
|
18
|
+
def type
|
19
|
+
@question.dig(:entry, :interaction_data, :rce) ? "longtextV2" : "plaintext"
|
20
|
+
end
|
21
|
+
|
22
|
+
def rubric
|
23
|
+
@question.dig(:entry, :scoring_data, :value)
|
24
|
+
end
|
25
|
+
|
26
|
+
def rubric_points
|
27
|
+
@question[:points_possible]
|
28
|
+
end
|
29
|
+
|
30
|
+
def show_word_limit
|
31
|
+
@question.dig(:entry, :interaction_data, :word_limit_enabled)
|
32
|
+
end
|
33
|
+
|
34
|
+
def show_word_count
|
35
|
+
@question.dig(:entry, :interaction_data, :word_count)
|
36
|
+
end
|
37
|
+
|
38
|
+
def max_length
|
39
|
+
if @question.dig(:entry,
|
40
|
+
:interaction_data,
|
41
|
+
:word_limit_enabled)
|
42
|
+
@question.dig(:entry, :interaction_data, :word_limit_max).to_i
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def convert
|
47
|
+
super
|
48
|
+
begin
|
49
|
+
@converted_question = {
|
50
|
+
stimulus:,
|
51
|
+
spellcheck:,
|
52
|
+
type:,
|
53
|
+
metadata: {
|
54
|
+
rubric:,
|
55
|
+
rubric_points:
|
56
|
+
},
|
57
|
+
max_length:,
|
58
|
+
show_word_count:
|
59
|
+
}
|
60
|
+
|
61
|
+
if type == "plaintext"
|
62
|
+
@converted_question[:show_copy] = true
|
63
|
+
@converted_question[:show_cut] = true
|
64
|
+
@converted_question[:show_paste] = true
|
65
|
+
end
|
66
|
+
|
67
|
+
@converted_question[:show_word_limit] = "off" unless show_word_limit
|
68
|
+
rescue => e
|
69
|
+
@conversion_errors << e.message
|
70
|
+
end
|
71
|
+
post_convert
|
72
|
+
format
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -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 FileUpload < 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 File upload 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,180 @@
|
|
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
|
+
# This class converts 'Fill in the Blank' questions from Canvas New Quizzes to a MasteryConnect
|
10
|
+
# compatible question type. Mixed answer types are not supported, failing the conversion.
|
11
|
+
class FillInBlank < NewQuestion
|
12
|
+
def initialize(question)
|
13
|
+
super
|
14
|
+
@include_possible_responses = false
|
15
|
+
@answer_type = ""
|
16
|
+
@is_mixed_question_type = false
|
17
|
+
end
|
18
|
+
|
19
|
+
def convert
|
20
|
+
super
|
21
|
+
begin
|
22
|
+
@converted_question = converted_question
|
23
|
+
rescue => e
|
24
|
+
@conversion_errors << e.message
|
25
|
+
end
|
26
|
+
post_convert
|
27
|
+
format
|
28
|
+
end
|
29
|
+
|
30
|
+
def converted_question
|
31
|
+
determine_answer_type
|
32
|
+
{
|
33
|
+
stimulus: question_title,
|
34
|
+
template: convert_item_body,
|
35
|
+
type: @answer_type,
|
36
|
+
validation:,
|
37
|
+
possible_responses: (possible_responses if @include_possible_responses)
|
38
|
+
}.compact
|
39
|
+
end
|
40
|
+
|
41
|
+
def question_title
|
42
|
+
@question[:entry][:title]
|
43
|
+
end
|
44
|
+
|
45
|
+
def convert_item_body
|
46
|
+
html = Nokogiri::HTML::DocumentFragment.parse(@question[:entry][:item_body])
|
47
|
+
|
48
|
+
# Remove all HTML tags except the <span> elements with 'blank...' IDs
|
49
|
+
html.css('span[id^="blank_"]').each do |span|
|
50
|
+
span.replace("{{response}}")
|
51
|
+
end
|
52
|
+
|
53
|
+
html.to_xhtml(indent: 0)
|
54
|
+
end
|
55
|
+
|
56
|
+
def validation
|
57
|
+
all_responses = response_list
|
58
|
+
{
|
59
|
+
scoring_type: "exactMatch",
|
60
|
+
valid_response: all_responses.shift,
|
61
|
+
alt_responses: (all_responses unless all_responses.empty?),
|
62
|
+
match_all_possible_responses: (true unless all_responses.empty?)
|
63
|
+
}.compact
|
64
|
+
end
|
65
|
+
|
66
|
+
def response_list
|
67
|
+
response_list = Array.new(response_length) { { score: point_value, value: [] } }
|
68
|
+
|
69
|
+
# Iterate through question's value array and populate response_list
|
70
|
+
@question[:entry][:scoring_data][:value].each do |blank|
|
71
|
+
(0..(response_length - 1)).each do |i|
|
72
|
+
if !@is_mixed_question_type && @answer_type == "clozetext"
|
73
|
+
validate_scoring_algorithm(blank[:scoring_algorithm])
|
74
|
+
end
|
75
|
+
# Retrieve value at current index if it exists
|
76
|
+
if blank[:scoring_data][:value].is_a?(Array)
|
77
|
+
response_list[i][:value].append(val_from_array(i, blank))
|
78
|
+
else
|
79
|
+
response_list[i][:value].append(blank[:scoring_data][:blank_text])
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
response_list
|
85
|
+
end
|
86
|
+
|
87
|
+
def validate_scoring_algorithm(scoring_algorithm)
|
88
|
+
valid_algorithms = %w[TextEquivalence TextInChoices]
|
89
|
+
return if valid_algorithms.include?(scoring_algorithm)
|
90
|
+
|
91
|
+
@notifications << "Fill in the blank question with text match option #{map_scoring_algorithm(scoring_algorithm)} has been converted to exact match"
|
92
|
+
end
|
93
|
+
|
94
|
+
def map_scoring_algorithm(scoring_algorithm)
|
95
|
+
case scoring_algorithm
|
96
|
+
when "TextContainsAnswer"
|
97
|
+
'"Contains"'
|
98
|
+
when "TextCloseEnough"
|
99
|
+
'"Close Enough"'
|
100
|
+
when "TextRegex"
|
101
|
+
'"Regular Expression Match"'
|
102
|
+
else
|
103
|
+
raise JSON::ParserError, "Invalid scoring_algorithm"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def response_length
|
108
|
+
@question[:entry][:scoring_data][:value].map do |blank|
|
109
|
+
blank[:scoring_data][:value].is_a?(Array) ? blank[:scoring_data][:value].length : 1
|
110
|
+
end.max || -1
|
111
|
+
end
|
112
|
+
|
113
|
+
def val_from_array(index, blank)
|
114
|
+
(index < blank[:scoring_data][:value].length) ? blank[:scoring_data][:value][index] : nil
|
115
|
+
end
|
116
|
+
|
117
|
+
def determine_answer_type
|
118
|
+
# Fix for a Canvas API bug where the `anwer_type` key is `answerType`
|
119
|
+
answer_type_key = @question[:entry][:interaction_data][:blanks][0][:answer_type] ? :answer_type : :answerType
|
120
|
+
current_answer_type = @question[:entry][:interaction_data][:blanks][0][answer_type_key]
|
121
|
+
@question[:entry][:interaction_data][:blanks].each do |blank|
|
122
|
+
next unless blank[answer_type_key] != current_answer_type
|
123
|
+
|
124
|
+
@notifications << "Fill in the blank question with mixed answer types have been converted to open entry exact match"
|
125
|
+
current_answer_type = "openEntry"
|
126
|
+
@is_mixed_question_type = true
|
127
|
+
break
|
128
|
+
end
|
129
|
+
@answer_type = map_answer_type_to_mc_type(current_answer_type)
|
130
|
+
end
|
131
|
+
|
132
|
+
def map_answer_type_to_mc_type(answer_type)
|
133
|
+
case answer_type
|
134
|
+
when "openEntry"
|
135
|
+
"clozetext"
|
136
|
+
when "dropdown"
|
137
|
+
@include_possible_responses = true
|
138
|
+
"clozedropdown"
|
139
|
+
when "wordbank"
|
140
|
+
@include_possible_responses = true
|
141
|
+
"clozeassociation"
|
142
|
+
else
|
143
|
+
raise JSON::ParserError, "Invalid answer type for FillInBlank"
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def possible_responses
|
148
|
+
case @answer_type
|
149
|
+
when "clozedropdown"
|
150
|
+
dropdown_possible_responses
|
151
|
+
when "clozeassociation"
|
152
|
+
wordbank_possible_responses
|
153
|
+
else
|
154
|
+
raise JSON::ParserError, "There was an error converting this question"
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def dropdown_possible_responses
|
159
|
+
possible_responses = Array.new(@question[:entry][:interaction_data][:blanks].length) { [] }
|
160
|
+
@question[:entry][:interaction_data][:blanks].each_with_index do |blank, index|
|
161
|
+
blank[:choices].each do |choice|
|
162
|
+
possible_responses[index].append(choice[:item_body])
|
163
|
+
end
|
164
|
+
end
|
165
|
+
possible_responses
|
166
|
+
end
|
167
|
+
|
168
|
+
def wordbank_possible_responses
|
169
|
+
choices = @question.dig(:entry, :interaction_data, :word_bank_choices)
|
170
|
+
unless choices
|
171
|
+
raise JSON::ParserError, "Could not find `word_bank_choices` property to form possible responses"
|
172
|
+
end
|
173
|
+
|
174
|
+
choices.map { |choice| choice[:item_body] }
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|