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.
Files changed (116) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +9 -0
  3. data/README.md +33 -0
  4. data/lib/ams_migration/canvas/converter/classic_quiz/calculated.rb +33 -0
  5. data/lib/ams_migration/canvas/converter/classic_quiz/classic_question.rb +114 -0
  6. data/lib/ams_migration/canvas/converter/classic_quiz/essay.rb +36 -0
  7. data/lib/ams_migration/canvas/converter/classic_quiz/file_upload.rb +35 -0
  8. data/lib/ams_migration/canvas/converter/classic_quiz/fill_in_blank.rb +63 -0
  9. data/lib/ams_migration/canvas/converter/classic_quiz/fill_in_multiple_blanks.rb +111 -0
  10. data/lib/ams_migration/canvas/converter/classic_quiz/matching.rb +66 -0
  11. data/lib/ams_migration/canvas/converter/classic_quiz/multiple_answer.rb +27 -0
  12. data/lib/ams_migration/canvas/converter/classic_quiz/multiple_choice.rb +57 -0
  13. data/lib/ams_migration/canvas/converter/classic_quiz/multiple_dropdown.rb +126 -0
  14. data/lib/ams_migration/canvas/converter/classic_quiz/numerical.rb +89 -0
  15. data/lib/ams_migration/canvas/converter/classic_quiz/text.rb +28 -0
  16. data/lib/ams_migration/canvas/converter/classic_quiz/true_false.rb +14 -0
  17. data/lib/ams_migration/canvas/converter/classic_quiz/unknown.rb +31 -0
  18. data/lib/ams_migration/canvas/converter/new_quiz/categorization.rb +108 -0
  19. data/lib/ams_migration/canvas/converter/new_quiz/essay.rb +78 -0
  20. data/lib/ams_migration/canvas/converter/new_quiz/file_upload.rb +33 -0
  21. data/lib/ams_migration/canvas/converter/new_quiz/fill_in_blank.rb +180 -0
  22. data/lib/ams_migration/canvas/converter/new_quiz/formula.rb +33 -0
  23. data/lib/ams_migration/canvas/converter/new_quiz/hotspot.rb +166 -0
  24. data/lib/ams_migration/canvas/converter/new_quiz/matching.rb +85 -0
  25. data/lib/ams_migration/canvas/converter/new_quiz/multiple_answer.rb +28 -0
  26. data/lib/ams_migration/canvas/converter/new_quiz/multiple_choice.rb +70 -0
  27. data/lib/ams_migration/canvas/converter/new_quiz/new_question.rb +133 -0
  28. data/lib/ams_migration/canvas/converter/new_quiz/numerical.rb +117 -0
  29. data/lib/ams_migration/canvas/converter/new_quiz/ordering.rb +68 -0
  30. data/lib/ams_migration/canvas/converter/new_quiz/stimulus.rb +35 -0
  31. data/lib/ams_migration/canvas/converter/new_quiz/true_false.rb +33 -0
  32. data/lib/ams_migration/canvas/converter/new_quiz/unknown.rb +30 -0
  33. data/lib/ams_migration/canvas/converter/question_type.rb +30 -0
  34. data/lib/ams_migration/services/README.md +26 -0
  35. data/lib/ams_migration/services/data_provider.rb +26 -0
  36. data/lib/ams_migration/services/learnosity_bank_cleaner.rb +44 -0
  37. data/lib/ams_migration/services/learnosity_data_api_client.rb +135 -0
  38. data/lib/ams_migration/services/learnosity_sink.rb +190 -0
  39. data/lib/ams_migration/services/migration_service.rb +37 -0
  40. data/lib/ams_migration/services/new_quiz_learnosity_transformer.rb +115 -0
  41. data/lib/ams_migration/version.rb +3 -0
  42. data/lib/ams_migration.rb +45 -0
  43. data/lib/factories/classic_quiz_item_converter_factory.rb +43 -0
  44. data/lib/factories/new_quiz_item_converter_factory.rb +76 -0
  45. data/spec/fixtures/sample_data/classic_quiz/calculated.json +24 -0
  46. data/spec/fixtures/sample_data/classic_quiz/essay.json +24 -0
  47. data/spec/fixtures/sample_data/classic_quiz/file_upload.json +24 -0
  48. data/spec/fixtures/sample_data/classic_quiz/fill_in_bank.json +24 -0
  49. data/spec/fixtures/sample_data/classic_quiz/fill_in_blank.json +53 -0
  50. data/spec/fixtures/sample_data/classic_quiz/fill_in_multiple_blanks.json +57 -0
  51. data/spec/fixtures/sample_data/classic_quiz/matching.json +107 -0
  52. data/spec/fixtures/sample_data/classic_quiz/multiple_answer.json +48 -0
  53. data/spec/fixtures/sample_data/classic_quiz/multiple_choice.json +56 -0
  54. data/spec/fixtures/sample_data/classic_quiz/multiple_dropdown.json +73 -0
  55. data/spec/fixtures/sample_data/classic_quiz/numerical.json +55 -0
  56. data/spec/fixtures/sample_data/classic_quiz/text.json +24 -0
  57. data/spec/fixtures/sample_data/classic_quiz/true_false.json +39 -0
  58. data/spec/fixtures/sample_data/classic_quiz/unknown.json +24 -0
  59. data/spec/fixtures/sample_data/new_quiz/bank_entry_item.json +5 -0
  60. data/spec/fixtures/sample_data/new_quiz/bank_item.json +11 -0
  61. data/spec/fixtures/sample_data/new_quiz/categorization.json +135 -0
  62. data/spec/fixtures/sample_data/new_quiz/essay.json +40 -0
  63. data/spec/fixtures/sample_data/new_quiz/file_upload.json +30 -0
  64. data/spec/fixtures/sample_data/new_quiz/fill_in_blank_association.json +162 -0
  65. data/spec/fixtures/sample_data/new_quiz/fill_in_blank_dropdown.json +180 -0
  66. data/spec/fixtures/sample_data/new_quiz/fill_in_blank_math.json +45 -0
  67. data/spec/fixtures/sample_data/new_quiz/fill_in_blank_mixed.json +188 -0
  68. data/spec/fixtures/sample_data/new_quiz/fill_in_blank_text.json +140 -0
  69. data/spec/fixtures/sample_data/new_quiz/formula.json +51 -0
  70. data/spec/fixtures/sample_data/new_quiz/hotspot_oval.json +38 -0
  71. data/spec/fixtures/sample_data/new_quiz/hotspot_polygon.json +66 -0
  72. data/spec/fixtures/sample_data/new_quiz/hotspot_square.json +38 -0
  73. data/spec/fixtures/sample_data/new_quiz/matching.json +88 -0
  74. data/spec/fixtures/sample_data/new_quiz/multiple_answer.json +57 -0
  75. data/spec/fixtures/sample_data/new_quiz/multiple_choice.json +66 -0
  76. data/spec/fixtures/sample_data/new_quiz/multiple_choice_math.json +55 -0
  77. data/spec/fixtures/sample_data/new_quiz/numerical.json +64 -0
  78. data/spec/fixtures/sample_data/new_quiz/ordering.json +60 -0
  79. data/spec/fixtures/sample_data/new_quiz/stimulus.json +17 -0
  80. data/spec/fixtures/sample_data/new_quiz/true_false.json +27 -0
  81. data/spec/fixtures/sample_data/new_quiz/unknown.json +14 -0
  82. data/spec/lib/ams_migration/services/learnosity_sink_spec.rb +198 -0
  83. data/spec/lib/canvas/converter/classic_quiz/calculated_spec.rb +26 -0
  84. data/spec/lib/canvas/converter/classic_quiz/essay_spec.rb +26 -0
  85. data/spec/lib/canvas/converter/classic_quiz/file_upload_spec.rb +23 -0
  86. data/spec/lib/canvas/converter/classic_quiz/fill_in_blank_spec.rb +30 -0
  87. data/spec/lib/canvas/converter/classic_quiz/fill_in_multiple_blanks_spec.rb +33 -0
  88. data/spec/lib/canvas/converter/classic_quiz/matching_spec.rb +38 -0
  89. data/spec/lib/canvas/converter/classic_quiz/multiple_answer_spec.rb +33 -0
  90. data/spec/lib/canvas/converter/classic_quiz/multiple_choice_spec.rb +31 -0
  91. data/spec/lib/canvas/converter/classic_quiz/multiple_dropdown_spec.rb +39 -0
  92. data/spec/lib/canvas/converter/classic_quiz/numerical_spec.rb +41 -0
  93. data/spec/lib/canvas/converter/classic_quiz/text_spec.rb +20 -0
  94. data/spec/lib/canvas/converter/classic_quiz/true_false_spec.rb +28 -0
  95. data/spec/lib/canvas/converter/classic_quiz/unknown_spec.rb +25 -0
  96. data/spec/lib/canvas/converter/common_error.rb +16 -0
  97. data/spec/lib/canvas/converter/new_quiz/categorization_spec.rb +55 -0
  98. data/spec/lib/canvas/converter/new_quiz/essay_spec.rb +26 -0
  99. data/spec/lib/canvas/converter/new_quiz/file_upload_spec.rb +24 -0
  100. data/spec/lib/canvas/converter/new_quiz/fill_in_blank_spec.rb +111 -0
  101. data/spec/lib/canvas/converter/new_quiz/formula_spec.rb +23 -0
  102. data/spec/lib/canvas/converter/new_quiz/hotspot_spec.rb +182 -0
  103. data/spec/lib/canvas/converter/new_quiz/matching_spec.rb +30 -0
  104. data/spec/lib/canvas/converter/new_quiz/multiple_answer_spec.rb +34 -0
  105. data/spec/lib/canvas/converter/new_quiz/multiple_choice_spec.rb +54 -0
  106. data/spec/lib/canvas/converter/new_quiz/numerical_spec.rb +48 -0
  107. data/spec/lib/canvas/converter/new_quiz/ordering_spec.rb +25 -0
  108. data/spec/lib/canvas/converter/new_quiz/stimulus_spec.rb +20 -0
  109. data/spec/lib/canvas/converter/new_quiz/true_false_spec.rb +24 -0
  110. data/spec/lib/canvas/converter/new_quiz/unknown_spec.rb +36 -0
  111. data/spec/lib/factories/classic_quiz_item_converter_factory_spec.rb +183 -0
  112. data/spec/lib/factories/new_quiz_item_converter_factory_spec.rb +71 -0
  113. data/spec/lib/services/learnosity_data_api_client_spec.rb +130 -0
  114. data/spec/lib/services/new_quiz_learnosity_transformer_spec.rb +196 -0
  115. data/spec/spec_helper.rb +20 -0
  116. metadata +229 -0
@@ -0,0 +1,198 @@
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
+
8
+ describe AmsMigration::Services::LearnositySink do
9
+ let(:config) do
10
+ {
11
+ consumer_key: "test_key",
12
+ consumer_secret: "test_secret",
13
+ domain: "test.domain",
14
+ organization_id: "test_org",
15
+ replace_images: true,
16
+ new_image_prefix: "test_prefix"
17
+ }
18
+ end
19
+
20
+ let(:sink) { described_class.new(config) }
21
+ let(:client) { instance_double(AmsMigration::Services::LearnosityDataApiClient) }
22
+
23
+ before do
24
+ allow(AmsMigration::Services::LearnosityDataApiClient).to receive(:new).and_return(client)
25
+ end
26
+
27
+ describe "#replace_images?" do
28
+ context "when replace_images is true in config" do
29
+ let(:config) { super().merge(replace_images: true) }
30
+
31
+ it "returns true" do
32
+ expect(sink.send(:replace_images?)).to be true
33
+ end
34
+ end
35
+
36
+ context "when replace_images is false in config" do
37
+ let(:config) { super().merge(replace_images: false) }
38
+
39
+ it "returns false" do
40
+ expect(sink.send(:replace_images?)).to be false
41
+ end
42
+ end
43
+
44
+ context "when replace_images is not specified in config" do
45
+ let(:config) do
46
+ super().tap { |c| c.delete(:replace_images) }
47
+ end
48
+
49
+ it "defaults to false" do
50
+ expect(sink.send(:replace_images?)).to be false
51
+ end
52
+ end
53
+ end
54
+
55
+ describe "#replace_images_in_questions" do
56
+ let(:questions) do
57
+ [
58
+ {
59
+ "data" => {
60
+ "stimulus" => '<p>Here is an image: <img src="http://example.com/image1.jpg"></p>',
61
+ "options" => [
62
+ { "label" => '<img src="http://example.com/image2.png">' }
63
+ ]
64
+ }
65
+ }
66
+ ]
67
+ end
68
+
69
+ let(:image_map) do
70
+ {
71
+ "http://example.com/image1.jpg" => "https://assets.learnosity.com/test_prefix/image1.jpg",
72
+ "http://example.com/image2.png" => "https://assets.learnosity.com/test_prefix/image2.png"
73
+ }
74
+ end
75
+
76
+ it "replaces image URLs in the collection" do
77
+ expect(sink).to receive(:upload_images).with(["http://example.com/image1.jpg", "http://example.com/image2.png"]).and_return(image_map)
78
+
79
+ result = sink.send(:replace_images_in_questions, questions)
80
+ question = result.first["data"]
81
+ expect(question["stimulus"]).to include('src="https://assets.learnosity.com/test_prefix/image1.jpg"')
82
+ expect(question["options"].first["label"]).to include('src="https://assets.learnosity.com/test_prefix/image2.png"')
83
+ end
84
+ end
85
+
86
+ describe "#find_images_in_json" do
87
+ it "finds image sources in nested JSON structures" do
88
+ json = {
89
+ "stimulus" => '<p><img src="http://example.com/image1.jpg"></p>',
90
+ "options" => [
91
+ { "label" => '<img src="http://example.com/image2.png">' },
92
+ { "label" => "No image here" }
93
+ ],
94
+ "metadata" => {
95
+ "image" => '<img src="http://example.com/image3.gif">'
96
+ }
97
+ }
98
+
99
+ result = sink.send(:find_images_in_json, json)
100
+
101
+ expect(result).to match_array(%w[http://example.com/image1.jpg http://example.com/image2.png http://example.com/image3.gif])
102
+ end
103
+ end
104
+
105
+ describe "#find_images_in_question" do
106
+ context "with a regular question" do
107
+ let(:question) do
108
+ {
109
+ "type" => "mcq",
110
+ "data" => {
111
+ "stimulus" => '<p><img src="http://example.com/stimulus.jpg"></p>',
112
+ "options" => [
113
+ { "label" => '<img src="http://example.com/option1.png">' }
114
+ ]
115
+ }
116
+ }
117
+ end
118
+
119
+ it "finds images in stimulus and options" do
120
+ result = sink.send(:find_images_in_question, question)
121
+ expect(result).to match_array(%w[http://example.com/stimulus.jpg http://example.com/option1.png])
122
+ end
123
+ end
124
+
125
+ context "with a hotspot question" do
126
+ let(:question) do
127
+ {
128
+ "type" => "hotspot",
129
+ "data" => {
130
+ "type" => "hotspot",
131
+ "stimulus" => '<p><img src="http://example.com/stimulus.jpg"></p>',
132
+ "image" => {
133
+ "source" => "http://example.com/hotspot_background.jpg",
134
+ "width" => 600,
135
+ "height" => 400
136
+ },
137
+ "options" => [
138
+ { "label" => '<img src="http://example.com/option1.png">' }
139
+ ]
140
+ }
141
+ }
142
+ end
143
+
144
+ it "includes the hotspot background image" do
145
+ result = sink.send(:find_images_in_question, question)
146
+ expect(result).to match_array(%w[http://example.com/stimulus.jpg http://example.com/option1.png http://example.com/hotspot_background.jpg])
147
+ end
148
+ end
149
+
150
+ context "with invalid question data" do
151
+ it "handles nil question" do
152
+ expect(sink.send(:find_images_in_question, nil)).to eq([])
153
+ end
154
+
155
+ it "handles question without data" do
156
+ expect(sink.send(:find_images_in_question, { "type" => "mcq" })).to eq([])
157
+ end
158
+ end
159
+ end
160
+
161
+ describe "#replace_images_in_json" do
162
+ let(:image_map) do
163
+ {
164
+ "http://example.com/old1.jpg" => "https://assets.learnosity.com/new1.jpg",
165
+ "http://example.com/old2.png" => "https://assets.learnosity.com/new2.png"
166
+ }
167
+ end
168
+
169
+ it "replaces image URLs in nested JSON structures" do
170
+ json = {
171
+ "text" => '<p><img src="http://example.com/old1.jpg"></p>',
172
+ "nested" => {
173
+ "content" => '<img src="http://example.com/old2.png">'
174
+ },
175
+ "array" => [
176
+ { "image" => '<img src="http://example.com/old1.jpg">' }
177
+ ]
178
+ }
179
+
180
+ result = sink.send(:replace_images_in_json, json, image_map)
181
+
182
+ expect(result["text"]).to include('src="https://assets.learnosity.com/new1.jpg"')
183
+ expect(result["nested"]["content"]).to include('src="https://assets.learnosity.com/new2.png"')
184
+ expect(result["array"].first["image"]).to include('src="https://assets.learnosity.com/new1.jpg"')
185
+ end
186
+
187
+ it "handles non-string values" do
188
+ json = {
189
+ "number" => 42,
190
+ "boolean" => true,
191
+ "null" => nil
192
+ }
193
+
194
+ result = sink.send(:replace_images_in_json, json, image_map)
195
+ expect(result).to eq(json)
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,26 @@
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::ClassicQuiz::Calculated do
9
+ let(:question) { File.read(File.join(SPEC_ROOT, "fixtures", "sample_data", "classic_quiz", "calculated.json")) }
10
+
11
+ subject { described_class.new(question) }
12
+
13
+ it_behaves_like "handles common errors"
14
+
15
+ it "converts to essay item with placeholder text" do
16
+ conversion = subject.convert
17
+ expect(conversion[:question]).to eq(
18
+ {
19
+ stimulus: "The Calculated item was removed from the quiz because there is no equivalent item type in Mastery Connect. Please check the original quiz for this question.",
20
+ type: "unsupported_canvas_question_type",
21
+ validation: { valid_response: { score: 0, value: "" }, scoring_type: "exactMatch" },
22
+ is_math: true
23
+ }
24
+ )
25
+ end
26
+ end
@@ -0,0 +1,26 @@
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::ClassicQuiz::Essay do
9
+ let(:question) { File.read(File.join(SPEC_ROOT, "fixtures", "sample_data", "classic_quiz", "essay.json")) }
10
+
11
+ let!(:image_src) { '<img src="https://dummyfileurl.com/file.png" alt="file.png" data-api-endpoint="https://dummyfileurl.com" data-api-returntype="File" />' }
12
+
13
+ subject { described_class.new(question) }
14
+
15
+ it_behaves_like "handles common errors"
16
+
17
+ it "converts to essay item" do
18
+ conversion = subject.convert
19
+ expect(conversion[:question]).to eq({
20
+ stimulus: "<p>This is an essay</p> #{image_src}",
21
+ type: "longtextV2",
22
+ metadata: { rubric: "Rubric", rubric_points: 1 },
23
+ is_math: true
24
+ })
25
+ end
26
+ end
@@ -0,0 +1,23 @@
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::ClassicQuiz::FileUpload do
9
+ let(:question) { File.read(File.join(SPEC_ROOT, "fixtures", "sample_data", "classic_quiz", "file_upload.json")) }
10
+
11
+ subject { described_class.new(question) }
12
+
13
+ it_behaves_like "handles common errors"
14
+
15
+ it "converts to essay item with placeholder text" do
16
+ conversion = subject.convert
17
+ expect(conversion[:question]).to eq({ 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.",
18
+ type: "unsupported_canvas_question_type",
19
+ validation: { valid_response: { score: 0, value: "" },
20
+ scoring_type: "exactMatch" },
21
+ is_math: true })
22
+ end
23
+ end
@@ -0,0 +1,30 @@
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::ClassicQuiz::FillInBlank do
9
+ let(:question) { File.read(File.join(SPEC_ROOT, "fixtures", "sample_data", "classic_quiz", "fill_in_blank.json")) }
10
+ let!(:image_src) { '<img src="https://dummyfileurl.com/file.png" alt="file.png" data-api-endpoint="https://dummyfileurl.com" data-api-returntype="File" />' }
11
+
12
+ subject { described_class.new(question) }
13
+
14
+ it_behaves_like "handles common errors"
15
+
16
+ it "converts to clozetext item" do
17
+ conversion = subject.convert
18
+ expect(conversion[:question]).to eq({
19
+ template: "<p>This is the fill in the blank</p> #{image_src}<br />{{response}}",
20
+ type: "clozetext",
21
+ validation: { valid_response: { score: 1, value: ["answer 1"] },
22
+ scoring_type: "exactMatch" },
23
+ is_math: true,
24
+ metadata: { distractor_rationale_response_level: ["<p>answer comment 1</p>",
25
+ "<p>answer comment 2</p>",
26
+ "<p>answer comment 3</p>",
27
+ "<p>answer comment 4</p>"] }
28
+ })
29
+ end
30
+ 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
+ require "spec_helper"
6
+ require_relative "../common_error"
7
+
8
+ describe AmsMigration::Canvas::Converter::ClassicQuiz::FillInMultipleBlanks do
9
+ let(:question) do
10
+ File.read(File.join(SPEC_ROOT, "fixtures", "sample_data", "classic_quiz", "fill_in_multiple_blanks.json"))
11
+ end
12
+ let!(:image_src) { '<img src="https://dummyfileurl.com/file.png" alt="file.png" data-api-endpoint="https://dummyfileurl.com" data-api-returntype="File" />' }
13
+ subject { described_class.new(question) }
14
+
15
+ it_behaves_like "handles common errors"
16
+
17
+ it "converts to clozetext item" do
18
+ conversion = subject.convert
19
+ expect(conversion[:question]).to eq({
20
+ template: "<p><span>Roses are {{response}}color1, violets are {{response}}color2</span></p> #{image_src}",
21
+ type: "clozetext",
22
+ validation: { valid_response: { score: 1, value: %w[red green] },
23
+ scoring_type: "exactMatch",
24
+ match_all_possible_responses: true,
25
+ alt_responses: [{ score: 1, value: %w[blue pink] }] },
26
+ is_math: true,
27
+ metadata: { distractor_rationale_response_level: ["<p>red comments</p>",
28
+ "<p>blue comments</p>",
29
+ "<p>green comments</p>",
30
+ "<p>pink comments</p>"] }
31
+ })
32
+ end
33
+ end
@@ -0,0 +1,38 @@
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::ClassicQuiz::Matching do
9
+ let(:question) { File.read(File.join(SPEC_ROOT, "fixtures", "sample_data", "classic_quiz", "matching.json")) }
10
+
11
+ let!(:image_src) { '<img src="https://dummyfileurl.com/file.png" alt="file.png" data-api-endpoint="https://dummyfileurl.com" data-api-returntype="File" />' }
12
+
13
+ subject { described_class.new(question) }
14
+
15
+ it_behaves_like "handles common errors"
16
+
17
+ it "converts to association item" do
18
+ conversion = subject.convert
19
+ expect(conversion[:question]).to eq({
20
+ stimulus: "<p>Matching question</p> #{image_src}",
21
+ possible_responses: ["",
22
+ "",
23
+ "R1",
24
+ "R2",
25
+ "R3",
26
+ "d1",
27
+ "d2",
28
+ "d3",
29
+ "d4",
30
+ "d5"],
31
+ stimulus_list: ["", "", "L1", "L2", "L3"],
32
+ type: "association",
33
+ validation: { valid_response: { score: 1, value: ["", "", "R1", "R2", "R3"] },
34
+ scoring_type: "exactMatch" },
35
+ is_math: true
36
+ })
37
+ end
38
+ 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
+ require "spec_helper"
6
+ require_relative "../common_error"
7
+
8
+ describe AmsMigration::Canvas::Converter::ClassicQuiz::MultipleAnswer do
9
+ let(:question) { File.read(File.join(SPEC_ROOT, "fixtures", "sample_data", "classic_quiz", "multiple_answer.json")) }
10
+ let!(:image_src) { '<img src="https://dummyfileurl.com/file.png" alt="file.png" data-api-endpoint="https://dummyfileurl.com" data-api-returntype="File" />' }
11
+
12
+ subject { described_class.new(question) }
13
+
14
+ it_behaves_like "handles common errors"
15
+
16
+ it "converts to mcq item" do
17
+ conversion = subject.convert
18
+ expect(conversion[:question]).to eq({
19
+ stimulus: "<p>Multiple answer text</p> #{image_src}",
20
+ options: [{ label: "<p>Answer 1</p>", value: "6739" },
21
+ { label: "<p>Answer 2</p>", value: "1316" },
22
+ { label: "Answer 3", value: "6779" }],
23
+ type: "mcq",
24
+ validation: {
25
+ valid_response: { score: 1,
26
+ value: %w[6739 1316] },
27
+ scoring_type: "exactMatch"
28
+ },
29
+ is_math: true,
30
+ multiple_responses: true
31
+ })
32
+ end
33
+ 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
+ require "spec_helper"
6
+ require_relative "../common_error"
7
+
8
+ describe AmsMigration::Canvas::Converter::ClassicQuiz::MultipleChoice do
9
+ let(:question) { File.read(File.join(SPEC_ROOT, "fixtures", "sample_data", "classic_quiz", "multiple_choice.json")) }
10
+ let!(:image_src) { '<img src="https://dummyfileurl.com/file.png" alt="file.png" data-api-endpoint="https://dummyfileurl.com" data-api-returntype="File" />' }
11
+
12
+ subject { described_class.new(question) }
13
+
14
+ it_behaves_like "handles common errors"
15
+
16
+ it "converts to mcq item" do
17
+ conversion = subject.convert
18
+ expect(conversion[:question]).to eq({
19
+ stimulus: "<p>This is an MC Item</p><p><math xmlns=\"http://www.w3.org/1998/Math/MathML\"> <munderover> <mo>∑<!-- ∑ --></mo> <mrow class=\"MJX-TeXAtom-ORD\"> <mn>23</mn> </mrow> <mrow class=\"MJX-TeXAtom-ORD\"> <mn>23</mn> </mrow> </munderover> <mi>w</mi> <mi>w</mi> <mi>w</mi> <mo>+</mo> <mi>f</mi> <mrow> <mo>(</mo> <mi>d</mi> <mo>)</mo> </mrow></math></p><p> </p> #{image_src}",
20
+ options: [{ label: "<p>Correct answer</p>", value: "2483" },
21
+ { label: "<p>Answer B<strong>more markup</strong></p>",
22
+ value: "5396" },
23
+ { label: "cc", value: "4310" },
24
+ { label: "dd", value: "7473" }],
25
+ type: "mcq",
26
+ validation: { valid_response: { score: 1, value: ["2483"] },
27
+ scoring_type: "exactMatch" },
28
+ is_math: true
29
+ })
30
+ end
31
+ end
@@ -0,0 +1,39 @@
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::ClassicQuiz::MultipleDropdown do
9
+ let(:question) do
10
+ File.read(File.join(SPEC_ROOT, "fixtures", "sample_data", "classic_quiz", "multiple_dropdown.json"))
11
+ end
12
+ let!(:image_src) { '<img src="https://dummyfileurl.com/file.png" alt="file.png" data-api-endpoint="https://dummyfileurl.com" data-api-returntype="File" />' }
13
+
14
+ subject { described_class.new(question) }
15
+
16
+ it_behaves_like "handles common errors"
17
+
18
+ it "converts to clozedropdown item" do
19
+ conversion = subject.convert
20
+ expect(conversion[:question]).to eq({
21
+ template: "<p><span>Roses are {{response}}color1, violets are {{response}}color2</span></p><p> </p> #{image_src}",
22
+ type: "clozedropdown",
23
+ possible_responses: [["red", "blue", "green"], ["green", "", "black"]],
24
+ validation: { valid_response: { score: 1, value: %w[red green] },
25
+ scoring_type: "exactMatch",
26
+ match_all_possible_responses: true,
27
+ alt_responses: [{ score: 1, value: ["blue", ""] },
28
+ { score: 1, value: %w[green black] }] },
29
+ is_math: true,
30
+ response_container: { pointer: "left" },
31
+ metadata: { distractor_rationale_response_level: ["<p>answer 1 comments</p>",
32
+ "<p>answer 2 comments</p>",
33
+ "<p>answer 3 comments</p>",
34
+ "",
35
+ "",
36
+ ""] }
37
+ })
38
+ end
39
+ end
@@ -0,0 +1,41 @@
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::ClassicQuiz::Numerical do
9
+ let(:question) { File.read(File.join(SPEC_ROOT, "fixtures", "sample_data", "classic_quiz", "numerical.json")) }
10
+ let!(:image_src) { '<img src="https://dummyfileurl.com/file.png" alt="file.png" data-api-endpoint="https://dummyfileurl.com" data-api-returntype="File" />' }
11
+
12
+ subject { described_class.new(question) }
13
+
14
+ it_behaves_like "handles common errors"
15
+
16
+ it "converts to clozeformula item" do
17
+ conversion = subject.convert
18
+ expect(conversion[:question]).to eq({
19
+ stimulus: "<p>Numerical answer stem</p> #{image_src}",
20
+ template: "{{response}}",
21
+ response_container: {
22
+ template: ""
23
+ },
24
+ type: "clozeformula",
25
+ validation: { scoring_type: "exactMatch",
26
+ valid_response: { score: 1,
27
+ value: [
28
+ [{ method: "equivLiteral", value: "20", options: { ignoreOrder: false, inverseResult: false } },
29
+ { method: "equivValue",
30
+ value: '4\\pm2',
31
+ options: { ignoreOrder: false,
32
+ inverseResult: false } },
33
+ { method: "equivValue",
34
+ value: "21.0001001",
35
+ options: { decimalPlaces: 10 } }]
36
+ ] } },
37
+ is_math: true,
38
+ response_containers: []
39
+ })
40
+ end
41
+ end
@@ -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::ClassicQuiz::Text do
9
+ let(:question) { File.read(File.join(SPEC_ROOT, "fixtures", "sample_data", "classic_quiz", "text.json")) }
10
+ let!(:image_src) { '<img src="https://dummyfileurl.com/file.png" alt="file.png" data-api-endpoint="https://dummyfileurl.com" data-api-returntype="File" />' }
11
+
12
+ subject { described_class.new(question) }
13
+
14
+ it_behaves_like "handles common errors"
15
+
16
+ it "converts to a text return value" do
17
+ conversion = subject.convert
18
+ expect(conversion[:question]).to eq("<p>Text here</p> #{image_src}")
19
+ end
20
+ 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
+ require "spec_helper"
6
+ require_relative "../common_error"
7
+
8
+ describe AmsMigration::Canvas::Converter::ClassicQuiz::TrueFalse do
9
+ let(:question) { File.read(File.join(SPEC_ROOT, "fixtures", "sample_data", "classic_quiz", "true_false.json")) }
10
+ let!(:image_src) { '<img src="https://dummyfileurl.com/file.png" alt="file.png" data-api-endpoint="https://dummyfileurl.com" data-api-returntype="File" />' }
11
+
12
+ subject { described_class.new(question) }
13
+
14
+ it_behaves_like "handles common errors"
15
+
16
+ it "converts to mcq item" do
17
+ conversion = subject.convert
18
+ expect(conversion[:question]).to eq({
19
+ stimulus: "<p>This is true/False</p> #{image_src}",
20
+ options: [{ label: "True", value: "8059" },
21
+ { label: "False", value: "957" }],
22
+ type: "mcq",
23
+ validation: { valid_response: { score: 1, value: ["957"] },
24
+ scoring_type: "exactMatch" },
25
+ is_math: true
26
+ })
27
+ end
28
+ end
@@ -0,0 +1,25 @@
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::ClassicQuiz::Unknown do
9
+ let(:question) do
10
+ File.read(File.join(SPEC_ROOT, "fixtures", "sample_data", "classic_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
+ end
@@ -0,0 +1,16 @@
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
+ shared_examples "handles common errors" do
6
+ it "returns with no errors" do
7
+ conversion = subject.convert
8
+ expect(conversion[:errors].length).to eq(0), "Errors: #{conversion[:errors].inspect}"
9
+ end
10
+
11
+ it "returns errors with malformed json" do
12
+ subject = described_class.new('{"this is": "not valid json" xyz }')
13
+ conversion = subject.convert
14
+ expect(conversion[:errors].length).to be >= 1
15
+ end
16
+ end