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,20 @@
|
|
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 "spec_helper"
|
6
|
+
require_relative "../common_error"
|
7
|
+
|
8
|
+
describe AmsMigration::Canvas::Converter::NewQuiz::Stimulus do
|
9
|
+
let(:question) { File.read(File.join(SPEC_ROOT, "fixtures", "sample_data", "new_quiz", "stimulus.json")) }
|
10
|
+
|
11
|
+
subject { described_class.new(question) }
|
12
|
+
|
13
|
+
it_behaves_like "handles common errors"
|
14
|
+
|
15
|
+
it "converts to a text return value" do
|
16
|
+
conversion = subject.convert
|
17
|
+
expect(conversion[:question]).to eq('<p><strong>Instructions</strong></p><p>Here are some instructions</p><p><strong>Stimulus Title</strong></p><p>The content in <sub>question</sub>. With <strong>some</strong> funky <span style="text-decoration: underline;">extra</span> <em>words</em> <span style="color: #f1c40f;">that</span> I can <span style="background-color: #843fa1;">format</span></p><p><img src="https://dummy-inscloudgate.net/files/3ec7226b-fd6d-49df-8236-cb52001ea10b?token=eyJ0eXAiOiJKV1QiLCJhbGciOi" alt="Alternative Text" /></p>')
|
18
|
+
expect(conversion[:title]).to eq("Stimulus Title")
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,24 @@
|
|
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 "spec_helper"
|
6
|
+
require_relative "../common_error"
|
7
|
+
|
8
|
+
describe AmsMigration::Canvas::Converter::NewQuiz::TrueFalse do
|
9
|
+
let(:question) { File.read(File.join(SPEC_ROOT, "fixtures", "sample_data", "new_quiz", "true_false.json")) }
|
10
|
+
|
11
|
+
subject { described_class.new(question) }
|
12
|
+
|
13
|
+
it_behaves_like "handles common errors"
|
14
|
+
|
15
|
+
it "converts to true false item" do
|
16
|
+
conversion = subject.convert
|
17
|
+
expect(conversion[:question]).to eq({ stimulus: '<p>This is<span style="color: #f1c40f;"> the</span> stem/stimulus <strong>with</strong> <span style="text-decoration: underline;">some</span> formatting</p><p><img src="https://dummy-inscloudgate.net/files/3ec7226b-fd6d-49df-8236-cb52001ea10b?token=eyJ0eXAiOiJKV1QiLCJhbGciOi" alt="Alternative Text" /></p>',
|
18
|
+
options: [{ label: "True", value: "0" },
|
19
|
+
{ label: "False", value: "1" }],
|
20
|
+
type: "mcq",
|
21
|
+
validation: { valid_response: { score: 1, value: ["0"] },
|
22
|
+
scoring_type: "exactMatch" } })
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,36 @@
|
|
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 "spec_helper"
|
6
|
+
require_relative "../common_error"
|
7
|
+
|
8
|
+
describe AmsMigration::Canvas::Converter::NewQuiz::Unknown do
|
9
|
+
let(:question) do
|
10
|
+
File.read(File.join(SPEC_ROOT, "fixtures", "sample_data", "new_quiz", "unknown.json"))
|
11
|
+
end
|
12
|
+
|
13
|
+
subject { described_class.new(question) }
|
14
|
+
|
15
|
+
it "returns errors with malformed json" do
|
16
|
+
subject = described_class.new('{"this is": "not valid json" xyz }')
|
17
|
+
conversion = subject.convert
|
18
|
+
expect(conversion[:errors].length).to be >= 1
|
19
|
+
end
|
20
|
+
|
21
|
+
it "returns with question type error" do
|
22
|
+
conversion = subject.convert
|
23
|
+
expect(conversion[:errors]).to include("Unknown question type: Unknown")
|
24
|
+
end
|
25
|
+
|
26
|
+
context "With unsupported Bank entry type" do
|
27
|
+
let(:question) do
|
28
|
+
File.read(File.join(SPEC_ROOT, "fixtures", "sample_data", "new_quiz", "bank_item.json"))
|
29
|
+
end
|
30
|
+
|
31
|
+
it "returns with entry type error" do
|
32
|
+
conversion = subject.convert
|
33
|
+
expect(conversion[:errors]).to include("Unsupported entry type: Bank")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,183 @@
|
|
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 "spec_helper"
|
6
|
+
|
7
|
+
describe AmsMigration::CanvasQuizzes::ClassicQuizItemConverterFactory do
|
8
|
+
describe ".create_converter" do
|
9
|
+
subject { described_class.create_converter(item) }
|
10
|
+
|
11
|
+
context "when item is a multiple choice question" do
|
12
|
+
let(:item) do
|
13
|
+
{
|
14
|
+
"question_type" => AmsMigration::Canvas::Converter::ClassicQuiz::ClassicQuestion::MULTI_CHOICE
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
it "returns a MultipleChoice converter" do
|
19
|
+
expect(subject).to be_a(AmsMigration::Canvas::Converter::ClassicQuiz::MultipleChoice)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
context "when item is a multiple answers question" do
|
24
|
+
let(:item) do
|
25
|
+
{
|
26
|
+
"question_type" => AmsMigration::Canvas::Converter::ClassicQuiz::ClassicQuestion::MULTIPLE_ANSWERS
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
it "returns a MultipleAnswer converter" do
|
31
|
+
expect(subject).to be_a(AmsMigration::Canvas::Converter::ClassicQuiz::MultipleAnswer)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
context "when item is a matching question" do
|
36
|
+
let(:item) do
|
37
|
+
{
|
38
|
+
"question_type" => AmsMigration::Canvas::Converter::ClassicQuiz::ClassicQuestion::MATCHING
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
it "returns a Matching converter" do
|
43
|
+
expect(subject).to be_a(AmsMigration::Canvas::Converter::ClassicQuiz::Matching)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
context "when item is a fill in blank question" do
|
48
|
+
let(:item) do
|
49
|
+
{
|
50
|
+
"question_type" => AmsMigration::Canvas::Converter::ClassicQuiz::ClassicQuestion::FILL_IN_BLANK
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
it "returns a FillInBlank converter" do
|
55
|
+
expect(subject).to be_a(AmsMigration::Canvas::Converter::ClassicQuiz::FillInBlank)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
context "when item is a fill in multiple blanks question" do
|
60
|
+
let(:item) do
|
61
|
+
{
|
62
|
+
"question_type" => AmsMigration::Canvas::Converter::ClassicQuiz::ClassicQuestion::FILL_IN_MULTIPLE_BLANKS
|
63
|
+
}
|
64
|
+
end
|
65
|
+
|
66
|
+
it "returns a FillInMultipleBlanks converter" do
|
67
|
+
expect(subject).to be_a(AmsMigration::Canvas::Converter::ClassicQuiz::FillInMultipleBlanks)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
context "when item is a multiple dropdowns question" do
|
72
|
+
let(:item) do
|
73
|
+
{
|
74
|
+
"question_type" => AmsMigration::Canvas::Converter::ClassicQuiz::ClassicQuestion::MULTIPLE_DROPDOWNS
|
75
|
+
}
|
76
|
+
end
|
77
|
+
|
78
|
+
it "returns a MultipleDropdown converter" do
|
79
|
+
expect(subject).to be_a(AmsMigration::Canvas::Converter::ClassicQuiz::MultipleDropdown)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
context "when item is a true/false question" do
|
84
|
+
let(:item) do
|
85
|
+
{
|
86
|
+
"question_type" => AmsMigration::Canvas::Converter::ClassicQuiz::ClassicQuestion::TRUE_FALSE
|
87
|
+
}
|
88
|
+
end
|
89
|
+
|
90
|
+
it "returns a TrueFalse converter" do
|
91
|
+
expect(subject).to be_a(AmsMigration::Canvas::Converter::ClassicQuiz::TrueFalse)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
context "when item is a numerical question" do
|
96
|
+
let(:item) do
|
97
|
+
{
|
98
|
+
"question_type" => AmsMigration::Canvas::Converter::ClassicQuiz::ClassicQuestion::NUMERICAL
|
99
|
+
}
|
100
|
+
end
|
101
|
+
|
102
|
+
it "returns a Numerical converter" do
|
103
|
+
expect(subject).to be_a(AmsMigration::Canvas::Converter::ClassicQuiz::Numerical)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
context "when item is a text only question" do
|
108
|
+
let(:item) do
|
109
|
+
{
|
110
|
+
"question_type" => AmsMigration::Canvas::Converter::ClassicQuiz::ClassicQuestion::TEXT_ONLY
|
111
|
+
}
|
112
|
+
end
|
113
|
+
|
114
|
+
it "returns a Text converter" do
|
115
|
+
expect(subject).to be_a(AmsMigration::Canvas::Converter::ClassicQuiz::Text)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
context "when item is a calculated question" do
|
120
|
+
let(:item) do
|
121
|
+
{
|
122
|
+
"question_type" => AmsMigration::Canvas::Converter::ClassicQuiz::ClassicQuestion::CALCULATED
|
123
|
+
}
|
124
|
+
end
|
125
|
+
|
126
|
+
it "returns a Calculated converter" do
|
127
|
+
expect(subject).to be_a(AmsMigration::Canvas::Converter::ClassicQuiz::Calculated)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
context "when item is a file upload question" do
|
132
|
+
let(:item) do
|
133
|
+
{
|
134
|
+
"question_type" => AmsMigration::Canvas::Converter::ClassicQuiz::ClassicQuestion::FILE_UPLOAD
|
135
|
+
}
|
136
|
+
end
|
137
|
+
|
138
|
+
it "returns a FileUpload converter" do
|
139
|
+
expect(subject).to be_a(AmsMigration::Canvas::Converter::ClassicQuiz::FileUpload)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
context "when item is an essay question" do
|
144
|
+
let(:item) do
|
145
|
+
{
|
146
|
+
"question_type" => AmsMigration::Canvas::Converter::ClassicQuiz::ClassicQuestion::ESSAY
|
147
|
+
}
|
148
|
+
end
|
149
|
+
|
150
|
+
it "returns an Essay converter" do
|
151
|
+
expect(subject).to be_a(AmsMigration::Canvas::Converter::ClassicQuiz::Essay)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
context "when item type is unknown" do
|
156
|
+
let(:item) do
|
157
|
+
{
|
158
|
+
"question_type" => "unknown-type"
|
159
|
+
}
|
160
|
+
end
|
161
|
+
|
162
|
+
it "returns an Unknown converter" do
|
163
|
+
expect(subject).to be_a(AmsMigration::Canvas::Converter::ClassicQuiz::Unknown)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
context "when converting item to json" do
|
168
|
+
let(:item) do
|
169
|
+
{
|
170
|
+
"question_type" => AmsMigration::Canvas::Converter::ClassicQuiz::ClassicQuestion::MULTI_CHOICE,
|
171
|
+
"question_text" => "Sample question"
|
172
|
+
}
|
173
|
+
end
|
174
|
+
|
175
|
+
it "passes the json representation to the converter" do
|
176
|
+
allow(AmsMigration::Canvas::Converter::ClassicQuiz::MultipleChoice).to receive(:new)
|
177
|
+
described_class.create_converter(item)
|
178
|
+
expect(AmsMigration::Canvas::Converter::ClassicQuiz::MultipleChoice)
|
179
|
+
.to have_received(:new).with(item.to_json)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
@@ -0,0 +1,71 @@
|
|
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 "spec_helper"
|
6
|
+
|
7
|
+
describe AmsMigration::CanvasQuizzes::NewQuizItemConverterFactory do
|
8
|
+
let(:item_type) { "unknown" }
|
9
|
+
let(:item) do
|
10
|
+
JSON.parse(File.read(File.join(SPEC_ROOT, "fixtures", "sample_data", "new_quiz", "#{item_type}.json")))
|
11
|
+
end
|
12
|
+
|
13
|
+
describe ".create_converter" do
|
14
|
+
subject { described_class.create_converter(item) }
|
15
|
+
context "when item is a multiple choice question" do
|
16
|
+
let(:item_type) { "multiple_choice" }
|
17
|
+
|
18
|
+
it "returns a MultipleChoice converter" do
|
19
|
+
expect(subject).to be_a(AmsMigration::Canvas::Converter::NewQuiz::MultipleChoice)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
context "when item is a multiple answer question" do
|
24
|
+
let(:item_type) { "multiple_answer" }
|
25
|
+
|
26
|
+
it "returns a MultipleAnswer converter" do
|
27
|
+
expect(subject).to be_a(AmsMigration::Canvas::Converter::NewQuiz::MultipleAnswer)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
context "when item is a matching question" do
|
32
|
+
let(:item_type) { "matching" }
|
33
|
+
|
34
|
+
it "returns a Matching converter" do
|
35
|
+
expect(subject).to be_a(AmsMigration::Canvas::Converter::NewQuiz::Matching)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
context "when item is a stimulus" do
|
40
|
+
let(:item_type) { "stimulus" }
|
41
|
+
|
42
|
+
it "returns a Stimulus converter" do
|
43
|
+
expect(subject).to be_a(AmsMigration::Canvas::Converter::NewQuiz::Stimulus)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# TODO: looks like it is not supported yet
|
48
|
+
context "when item is a bank entry" do
|
49
|
+
# let(:item_type) { 'bank_entry_item' }
|
50
|
+
let(:item) do
|
51
|
+
{
|
52
|
+
"entry_type" => "Item",
|
53
|
+
"archived" => false,
|
54
|
+
"entry" => {}
|
55
|
+
}
|
56
|
+
end
|
57
|
+
|
58
|
+
it "normalizes the item and returns appropriate converter" do
|
59
|
+
expect(subject).to be_a(AmsMigration::Canvas::Converter::NewQuiz::Unknown)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
context "when item type is unknown" do
|
64
|
+
let(:item_type) { "unknown" }
|
65
|
+
|
66
|
+
it "returns an Unknown converter" do
|
67
|
+
expect(subject).to be_a(AmsMigration::Canvas::Converter::NewQuiz::Unknown)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,130 @@
|
|
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 "spec_helper"
|
6
|
+
require "webmock/rspec"
|
7
|
+
require "logger"
|
8
|
+
|
9
|
+
describe AmsMigration::Services::LearnosityDataApiClient do
|
10
|
+
let(:config) do
|
11
|
+
{
|
12
|
+
consumer_key: "test_consumer_key",
|
13
|
+
consumer_secret: "test_consumer_secret",
|
14
|
+
domain: "test.domain.com",
|
15
|
+
data_api_url: "https://data.learnosity.com/v1",
|
16
|
+
organization_id: "test_org_id",
|
17
|
+
logger: Logger.new(nil)
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
subject(:client) { described_class.new(config) }
|
22
|
+
|
23
|
+
describe "#post_request" do
|
24
|
+
let(:request_data) { { "test_key" => "test_value" } }
|
25
|
+
let(:endpoint) { "activities" }
|
26
|
+
let(:method) { "get" }
|
27
|
+
let(:expected_url) { "#{config[:data_api_url]}/itembank/#{endpoint}" }
|
28
|
+
|
29
|
+
context "when the request is successful" do
|
30
|
+
let(:response_body) do
|
31
|
+
{
|
32
|
+
"meta" => { "status" => true },
|
33
|
+
"data" => [{ "id" => "123" }]
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
before do
|
38
|
+
stub_request(:post, expected_url)
|
39
|
+
.to_return(
|
40
|
+
status: 200,
|
41
|
+
body: response_body.to_json,
|
42
|
+
headers: { "Content-Type" => "application/json" }
|
43
|
+
)
|
44
|
+
end
|
45
|
+
|
46
|
+
it "makes a POST request with the correct parameters" do
|
47
|
+
result = client.post_request(request_data, endpoint, method)
|
48
|
+
|
49
|
+
expect(result).to eq(response_body)
|
50
|
+
expect(WebMock).to have_requested(:post, expected_url)
|
51
|
+
end
|
52
|
+
|
53
|
+
it "includes organization_id in the request" do
|
54
|
+
client.post_request(request_data, endpoint, method)
|
55
|
+
|
56
|
+
expect(WebMock).to(have_requested(:post, expected_url)
|
57
|
+
.with do |req|
|
58
|
+
form_data = URI.decode_www_form(req.body).to_h
|
59
|
+
json_data = JSON.parse(form_data["request"])
|
60
|
+
expect(json_data["organisation_id"]).to eq(config[:organization_id])
|
61
|
+
end)
|
62
|
+
end
|
63
|
+
|
64
|
+
it "signs the request using Learnosity SDK" do
|
65
|
+
client.post_request(request_data, endpoint, method)
|
66
|
+
|
67
|
+
expect(WebMock).to(have_requested(:post, expected_url)
|
68
|
+
.with do |req|
|
69
|
+
form_data = URI.decode_www_form(req.body).to_h
|
70
|
+
expect(form_data.keys).to include("security")
|
71
|
+
expect(form_data.keys).to include("request")
|
72
|
+
expect(form_data.keys).to include("action")
|
73
|
+
end)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
context "when the request fails" do
|
78
|
+
let(:error_response) do
|
79
|
+
{
|
80
|
+
"meta" => {
|
81
|
+
"status" => false,
|
82
|
+
"message" => "Invalid request"
|
83
|
+
},
|
84
|
+
"errors" => ["Something went wrong"]
|
85
|
+
}
|
86
|
+
end
|
87
|
+
|
88
|
+
before do
|
89
|
+
stub_request(:post, expected_url)
|
90
|
+
.to_return(
|
91
|
+
status: 400,
|
92
|
+
body: error_response.to_json,
|
93
|
+
headers: { "Content-Type" => "application/json" }
|
94
|
+
)
|
95
|
+
end
|
96
|
+
|
97
|
+
it "logs the error and returns the response" do
|
98
|
+
expect(config[:logger]).to receive(:debug).at_least(:once)
|
99
|
+
result = client.post_request(request_data, endpoint, method)
|
100
|
+
expect(result).to eq(error_response)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
context "when response body is empty" do
|
105
|
+
before do
|
106
|
+
stub_request(:post, expected_url)
|
107
|
+
.to_return(
|
108
|
+
status: 200,
|
109
|
+
body: "",
|
110
|
+
headers: { "Content-Type" => "application/json" }
|
111
|
+
)
|
112
|
+
end
|
113
|
+
|
114
|
+
it "handles empty response gracefully" do
|
115
|
+
result = client.post_request(request_data, endpoint, method)
|
116
|
+
expect(result).to be_nil
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
describe "#security_packet" do
|
122
|
+
it "returns a hash with consumer_key and domain" do
|
123
|
+
security = client.send(:security_packet)
|
124
|
+
expect(security).to eq({
|
125
|
+
"consumer_key" => config[:consumer_key],
|
126
|
+
"domain" => config[:domain]
|
127
|
+
})
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,196 @@
|
|
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 "spec_helper"
|
6
|
+
|
7
|
+
describe AmsMigration::Services::NewQuizLearnosityTransformer do
|
8
|
+
let(:root_account_uuid) { "test-account-123" }
|
9
|
+
let(:transformer) { described_class.new(root_account_uuid:) }
|
10
|
+
let(:quiz_id) { 456 }
|
11
|
+
let(:quiz_title) { "Sample Quiz" }
|
12
|
+
let(:item_id) { 789 }
|
13
|
+
let(:quiz_item) do
|
14
|
+
{
|
15
|
+
id: item_id,
|
16
|
+
position: 1,
|
17
|
+
title: "Sample Question"
|
18
|
+
}
|
19
|
+
end
|
20
|
+
let(:quiz) do
|
21
|
+
{
|
22
|
+
quiz: {
|
23
|
+
id: quiz_id,
|
24
|
+
title: quiz_title
|
25
|
+
},
|
26
|
+
quiz_items: [quiz_item]
|
27
|
+
}
|
28
|
+
end
|
29
|
+
let(:converted_item) do
|
30
|
+
{
|
31
|
+
title: "Sample Question",
|
32
|
+
question: {
|
33
|
+
type: "mcq",
|
34
|
+
stimulus: "What is the answer?",
|
35
|
+
options: []
|
36
|
+
}
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
before do
|
41
|
+
allow(AmsMigration::CanvasQuizzes::NewQuizItemConverterFactory).to receive(:create_converter)
|
42
|
+
.with(quiz_item)
|
43
|
+
.and_return(double(convert: converted_item))
|
44
|
+
end
|
45
|
+
|
46
|
+
describe "#transform" do
|
47
|
+
subject { transformer.transform(quiz) }
|
48
|
+
|
49
|
+
it "transforms quiz into Learnosity format" do
|
50
|
+
expect(subject).to include(
|
51
|
+
activities: [
|
52
|
+
{
|
53
|
+
"reference" => "a-nq-#{root_account_uuid}-#{quiz_id}",
|
54
|
+
"data" => {
|
55
|
+
"items" => ["i-nq-#{root_account_uuid}-#{item_id}"]
|
56
|
+
},
|
57
|
+
"title" => quiz_title,
|
58
|
+
"tags" => [
|
59
|
+
{ "type" => "import_original_id", "name" => quiz_id.to_s },
|
60
|
+
{ "type" => "import_account_id", "name" => root_account_uuid.to_s },
|
61
|
+
{ "type" => "import_system", "name" => "NQ" }
|
62
|
+
]
|
63
|
+
}
|
64
|
+
],
|
65
|
+
items: [
|
66
|
+
{
|
67
|
+
"reference" => "i-nq-#{root_account_uuid}-#{item_id}",
|
68
|
+
"title" => "Sample Question",
|
69
|
+
"status" => "published",
|
70
|
+
"definition" => {
|
71
|
+
"widgets" => [
|
72
|
+
{ "reference" => "q-nq-#{root_account_uuid}-#{item_id}" }
|
73
|
+
]
|
74
|
+
},
|
75
|
+
"tags" => [
|
76
|
+
{ "type" => "import_original_id", "name" => item_id.to_s },
|
77
|
+
{ "type" => "import_account_id", "name" => root_account_uuid.to_s },
|
78
|
+
{ "type" => "import_system", "name" => "NQ" }
|
79
|
+
]
|
80
|
+
}
|
81
|
+
],
|
82
|
+
questions: [
|
83
|
+
{
|
84
|
+
"reference" => "q-nq-#{root_account_uuid}-#{item_id}",
|
85
|
+
"type" => "mcq",
|
86
|
+
"data" => converted_item[:question]
|
87
|
+
}
|
88
|
+
],
|
89
|
+
features: {}
|
90
|
+
)
|
91
|
+
end
|
92
|
+
|
93
|
+
context "when question type is unsupported" do
|
94
|
+
let(:converted_item) do
|
95
|
+
{
|
96
|
+
title: "Unsupported Question",
|
97
|
+
question: {
|
98
|
+
type: "unsupported_canvas_question_type"
|
99
|
+
}
|
100
|
+
}
|
101
|
+
end
|
102
|
+
|
103
|
+
it "excludes unsupported questions from the result" do
|
104
|
+
expect(subject).to include(
|
105
|
+
activities: [
|
106
|
+
{
|
107
|
+
"reference" => "a-nq-#{root_account_uuid}-#{quiz_id}",
|
108
|
+
"data" => { "items" => [] },
|
109
|
+
"title" => quiz_title,
|
110
|
+
"tags" => [
|
111
|
+
{ "type" => "import_original_id", "name" => quiz_id.to_s },
|
112
|
+
{ "type" => "import_account_id", "name" => root_account_uuid.to_s },
|
113
|
+
{ "type" => "import_system", "name" => "NQ" }
|
114
|
+
]
|
115
|
+
}
|
116
|
+
],
|
117
|
+
items: [],
|
118
|
+
questions: [],
|
119
|
+
features: {}
|
120
|
+
)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
context "when question data is empty" do
|
125
|
+
let(:converted_item) do
|
126
|
+
{
|
127
|
+
title: "Empty Question",
|
128
|
+
question: {}
|
129
|
+
}
|
130
|
+
end
|
131
|
+
|
132
|
+
it "excludes empty questions from the result" do
|
133
|
+
expect(subject).to include(
|
134
|
+
activities: [
|
135
|
+
{
|
136
|
+
"reference" => "a-nq-#{root_account_uuid}-#{quiz_id}",
|
137
|
+
"data" => { "items" => [] },
|
138
|
+
"title" => quiz_title,
|
139
|
+
"tags" => [
|
140
|
+
{ "type" => "import_original_id", "name" => quiz_id.to_s },
|
141
|
+
{ "type" => "import_account_id", "name" => root_account_uuid.to_s },
|
142
|
+
{ "type" => "import_system", "name" => "NQ" }
|
143
|
+
]
|
144
|
+
}
|
145
|
+
],
|
146
|
+
items: [],
|
147
|
+
questions: [],
|
148
|
+
features: {}
|
149
|
+
)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
context "with multiple quiz items" do
|
154
|
+
let(:item_id_2) { 790 }
|
155
|
+
let(:quiz_item_2) do
|
156
|
+
{
|
157
|
+
id: item_id_2,
|
158
|
+
position: 2,
|
159
|
+
title: "Second Question"
|
160
|
+
}
|
161
|
+
end
|
162
|
+
let(:converted_item_2) do
|
163
|
+
{
|
164
|
+
title: "Second Question",
|
165
|
+
question: {
|
166
|
+
type: "mcq",
|
167
|
+
stimulus: "What is the second answer?",
|
168
|
+
options: []
|
169
|
+
}
|
170
|
+
}
|
171
|
+
end
|
172
|
+
|
173
|
+
before do
|
174
|
+
quiz[:quiz_items] << quiz_item_2
|
175
|
+
allow(AmsMigration::CanvasQuizzes::NewQuizItemConverterFactory).to receive(:create_converter)
|
176
|
+
.with(quiz_item_2)
|
177
|
+
.and_return(double(convert: converted_item_2))
|
178
|
+
end
|
179
|
+
|
180
|
+
it "transforms all quiz items in correct order" do
|
181
|
+
expect(subject[:activities].first["data"]["items"]).to eq([
|
182
|
+
"i-nq-#{root_account_uuid}-#{item_id}",
|
183
|
+
"i-nq-#{root_account_uuid}-#{item_id_2}"
|
184
|
+
])
|
185
|
+
expect(subject[:items].map { |i| i["reference"] }).to eq([
|
186
|
+
"i-nq-#{root_account_uuid}-#{item_id}",
|
187
|
+
"i-nq-#{root_account_uuid}-#{item_id_2}"
|
188
|
+
])
|
189
|
+
expect(subject[:questions].map { |q| q["reference"] }).to eq([
|
190
|
+
"q-nq-#{root_account_uuid}-#{item_id}",
|
191
|
+
"q-nq-#{root_account_uuid}-#{item_id_2}"
|
192
|
+
])
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,20 @@
|
|
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 "bundler/setup"
|
6
|
+
require "ams_migration"
|
7
|
+
require "webmock/rspec"
|
8
|
+
|
9
|
+
RSpec.configure do |config|
|
10
|
+
# Enable flags like --only-failures and --next-failure
|
11
|
+
config.example_status_persistence_file_path = ".rspec_status"
|
12
|
+
|
13
|
+
config.expect_with :rspec do |c|
|
14
|
+
c.syntax = :expect
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
SPEC_ROOT = File.expand_path(__dir__)
|
19
|
+
|
20
|
+
WebMock.disable_net_connect!(allow_localhost: true)
|