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,55 @@
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::Categorization do
9
+ let(:question) { File.read(File.join(SPEC_ROOT, "fixtures", "sample_data", "classic_quiz", "calculated.json")) }
10
+ let(:question) do
11
+ File.read(File.join(SPEC_ROOT,
12
+ "fixtures",
13
+ "sample_data",
14
+ "new_quiz",
15
+ "categorization.json"))
16
+ end
17
+
18
+ subject { described_class.new(question) }
19
+
20
+ it_behaves_like "handles common errors"
21
+
22
+ it "converts to a valid classification item" do
23
+ conversion = subject.convert
24
+ expect(conversion[:question]).to eq(
25
+ {
26
+ possible_responses: ["C3PO",
27
+ "JarJar Binks",
28
+ "Luke Skywalker",
29
+ "Darth Maul",
30
+ "Obi-Wan Kenobi",
31
+ "Darth Sidious",
32
+ "Ahsoka Tano",
33
+ "R2D2",
34
+ "Yoda",
35
+ "Mace Windu",
36
+ "Anakin Skywalker",
37
+ "Darth Tyranus"],
38
+ stimulus: "\u003cp\u003ePlease categorize the following \u003cstrong\u003eForce\u003c/strong\u003e users into the category that best fits them:\u003c/p\u003e\u003cp\u003e\u003cimg src=\"https://dummy-inscloudgate.net/files/3ec7226b-fd6d-49df-8236-cb52001ea10b?token=eyJ0eXAiOiJKV1QiLCJhbGciOi\" alt=\"Alternative Text\" /\u003e\u003c/p\u003e",
39
+ type: "classification",
40
+ ui_style: {
41
+ column_count: 3,
42
+ column_titles: ["Light side of the Force", "Dark side of the Force", "Gray side of the Force"]
43
+ },
44
+ validation: {
45
+ scoring_type: "exactMatch",
46
+ valid_response: {
47
+ score: 5,
48
+ value: [[4, 8, 9, 2], [3, 5, 11], [10, 6]]
49
+ }
50
+ }
51
+ }
52
+ )
53
+ expect(conversion[:title]).to eq("Categorization Question 1")
54
+ end
55
+ 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::NewQuiz::Essay do
9
+ let(:question) { File.read(File.join(SPEC_ROOT, "fixtures", "sample_data", "new_quiz", "essay.json")) }
10
+
11
+ subject { described_class.new(question) }
12
+
13
+ it_behaves_like "handles common errors"
14
+
15
+ it "converts to longtextV2 item" do
16
+ conversion = subject.convert
17
+ expect(conversion[:question]).to eq({ stimulus: "<p><strong>Question</strong> <em>stimulus</em> here <span style=\"text-decoration: underline;\">with</span> some <span style=\"text-decoration: underline;\"><em>extra</em></span> weird <span style=\"color: #000000;\">and</span> wacky <sup><span style=\"background-color: #e03e2d;\">words</span></sup> for testing of <span style=\"color: #fbeeb8;\">conversion</span></p>\u003cp\u003e\u003cimg src=\"https://dummy-inscloudgate.net/files/3ec7226b-fd6d-49df-8236-cb52001ea10b?token=eyJ0eXAiOiJKV1QiLCJhbGciOi\" alt=\"Alternative Text\" /\u003e\u003c/p\u003e",
18
+ spellcheck: true,
19
+ type: "longtextV2",
20
+ metadata: { rubric: "This is the rubric and should translate as such.",
21
+ rubric_points: 1.0 },
22
+ show_word_count: false,
23
+ show_word_limit: "off" })
24
+ expect(conversion[:title]).to eq("Rich Content Editor - no word limit, no word count, no spell check")
25
+ end
26
+ 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::FileUpload do
9
+ let(:question) { File.read(File.join(SPEC_ROOT, "fixtures", "sample_data", "new_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
+ expect(conversion[:title]).to eq("File Upload")
23
+ end
24
+ end
@@ -0,0 +1,111 @@
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::FillInBlank do
9
+ let(:file_name) { "" }
10
+ let(:question) do
11
+ File.read(File.join(SPEC_ROOT, "fixtures", "sample_data", "new_quiz", file_name))
12
+ end
13
+
14
+ subject { described_class.new(question) }
15
+
16
+ context "when new quiz question contains only text entry" do
17
+ let(:file_name) { "fill_in_blank_text.json" }
18
+
19
+ it_behaves_like "handles common errors"
20
+
21
+ it "converts to clozetext item" do
22
+ conversion = subject.convert
23
+ expect(conversion[:question]).to eq(
24
+ { stimulus: "Question 1",
25
+ template: '<p>Roses are {{response}},</p><p>Violets are {{response}}. </p><p>Sugar is {{response}}, </p><p>And so are {{response}}.</p><p>That is {{response}}, </p><p>I love you {{response}}.</p><p><img src="https://dummy-inscloudgate.net/files/3ec7226b-fd6d-49df-8236-cb52001ea10b?token=eyJ0eXAiOiJKV1QiLCJhbGciOi" alt="Alternative Text" /></p>',
26
+ type: "clozetext",
27
+ validation: { scoring_type: "exactMatch",
28
+ valid_response: { score: 1, value: %w[red blue sweet you why too] },
29
+ alt_responses: [{ score: 1, value: %w[red blue sweet youu why tooo] },
30
+ { score: 1, value: %w[red blue sweet youuu why toooo] }],
31
+ match_all_possible_responses: true } }
32
+ )
33
+ expect(conversion[:title]).to eq("Question 1")
34
+ end
35
+ end
36
+
37
+ context "when new quiz question contains only dropdown" do
38
+ let(:file_name) { "fill_in_blank_dropdown.json" }
39
+
40
+ it_behaves_like "handles common errors"
41
+
42
+ it "converts to clozedropdown item" do
43
+ conversion = subject.convert
44
+ expect(conversion[:question]).to eq(
45
+ { stimulus: "Question 1",
46
+ template: '<p>Roses are {{response}},</p><p>Violets are {{response}}. </p><p>Sugar is {{response}}, </p><p>And so are {{response}}.</p><p><img src="https://dummy-inscloudgate.net/files/3ec7226b-fd6d-49df-8236-cb52001ea10b?token=eyJ0eXAiOiJKV1QiLCJhbGciOi" alt="Alternative Text" /></p>',
47
+ type: "clozedropdown",
48
+ validation: { scoring_type: "exactMatch",
49
+ valid_response: { score: 1, value: %w[red blue sweet you] } },
50
+ possible_responses: [%w[red green yellow],
51
+ %w[blue purple gray],
52
+ %w[sweet sour bitter],
53
+ ["you", "they", "that guy"]] }
54
+ )
55
+ end
56
+ end
57
+
58
+ context "when new quiz question contains only association" do
59
+ let(:file_name) { "fill_in_blank_association.json" }
60
+
61
+ it_behaves_like "handles common errors"
62
+
63
+ it "converts to clozeassociation item" do
64
+ conversion = subject.convert
65
+ expect(conversion[:question]).to eq(
66
+ { stimulus: "Question 1",
67
+ template: '<p>Roses are {{response}},</p><p>Violets are {{response}}. </p><p>Sugar is {{response}}, </p><p>And so are {{response}}.</p><p><img src="https://dummy-inscloudgate.net/files/3ec7226b-fd6d-49df-8236-cb52001ea10b?token=eyJ0eXAiOiJKV1QiLCJhbGciOi" alt="Alternative Text" /></p>',
68
+ type: "clozeassociation",
69
+ validation: { scoring_type: "exactMatch",
70
+ valid_response: { score: 1, value: %w[red blue sweet you] } },
71
+ possible_responses: %w[green red blue sweet you purple orange sour bitter they] }
72
+ )
73
+ end
74
+ end
75
+
76
+ context "when new quiz question contains math in the template" do
77
+ let(:file_name) { "fill_in_blank_math.json" }
78
+
79
+ it "converts math to LaTeX format" do
80
+ conversion = subject.convert
81
+ expect(conversion[:question]).to eq(
82
+ { stimulus: "Question 1",
83
+ template: '<p>\\(2+2=\\) {{response}}<img src="https://dummy-inscloudgate.net/files/3ec7226b-fd6d-49df-8236-cb52001ea10b?token=eyJ0eXAiOiJKV1QiLCJhbGciOi" alt="Alternative Text" /></p>',
84
+ type: "clozetext",
85
+ validation: { scoring_type: "exactMatch", valid_response: { score: 1, value: ["4"] } },
86
+ is_math: true }
87
+ )
88
+ end
89
+ end
90
+
91
+ context "when new quiz question contains mixed answer types" do
92
+ let(:file_name) { "fill_in_blank_mixed.json" }
93
+
94
+ it "converts item as best it can and notifies user that additional configuration might be required" do
95
+ conversion = subject.convert
96
+ expect(conversion[:question]).to eq(
97
+ { stimulus: "Question 1",
98
+ template: "<p>Roses are {{response}}, violets are {{response}}.</p><p>Sugar is {{response}}, and so are {{response}}.</p><p>To each their {{response}}.</p><p>Bada bing, bada {{response}}.</p><p>Like two peas in a {{response}}.</p>",
99
+ type: "clozetext",
100
+ validation: { scoring_type: "exactMatch",
101
+ valid_response: { score: 1, value: %w[red blue sweet you own boom pod] },
102
+ alt_responses: [{ score: 1, value: %w[red blue sweet youu own boom pod] },
103
+ { score: 1, value: %w[red blue sweet youuu own boom pod] }],
104
+ match_all_possible_responses: true } }
105
+ )
106
+ expect(subject.notifications).to eq(
107
+ ["Fill in the blank question with mixed answer types have been converted to open entry exact match"]
108
+ )
109
+ end
110
+ end
111
+ 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::NewQuiz::Formula do
9
+ let(:question) { File.read(File.join(SPEC_ROOT, "fixtures", "sample_data", "new_quiz", "formula.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 Formula item was removed from the quiz because there is no equivalent item type in Mastery Connect. Please check the original quiz for this question.",
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,182 @@
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::Hotspot do
9
+ let(:file_name) { "" }
10
+ let(:question) do
11
+ File.read(File.join(SPEC_ROOT, "fixtures", "sample_data", "new_quiz", file_name))
12
+ end
13
+
14
+ before do
15
+ stub_request(:get, "https://dummyfileurl.com/file.png")
16
+ .with(
17
+ headers: {
18
+ "Accept" => "*/*",
19
+ "Accept-Encoding" => "identity",
20
+ "User-Agent" => "Ruby"
21
+ }
22
+ )
23
+ .to_return(status: 200, body: "", headers: {})
24
+ end
25
+
26
+ subject { described_class.new(question) }
27
+
28
+ context "when the hotspot type is square" do
29
+ let(:file_name) { "hotspot_square.json" }
30
+
31
+ it_behaves_like "handles common errors"
32
+
33
+ it "converts NQ JSON to hotspot item" do
34
+ conversion = subject.convert
35
+
36
+ expect(conversion[:question]).to eq({
37
+ type: "hotspot",
38
+ stimulus: "\u003cp\u003e\u003cem\u003e\u003cstrong\u003eThis\u003c/strong\u003e\u003c/em\u003e is a \u003cstrong\u003equestion\u003c/strong\u003e stem \u003cem\u003ewith\u003c/em\u003e some \u003cspan style=\"text-decoration: underline;\"\u003eadditional\u003c/span\u003e text to \u003cspan style=\"color: #ba372a;\"\u003etest \u003c/span\u003ewacky \u003cspan style=\"background-color: #fbeeb8;\"\u003eformatting\u003c/span\u003e\u003c/p\u003e\u003cp\u003e\\(234+\\alpha^4-56\\)\u003c/p\u003e\u003cp\u003eSome math is before this for \u003csub\u003etesting\u003c/sub\u003e\u003c/p\u003e\u003cp\u003e\u003cimg src=\"https://dummy-inscloudgate.net/files/3ec7226b-fd6d-49df-8236-cb52001ea10b?token=eyJ0eXAiOiJKV1QiLCJhbGciOi\" alt=\"Alternative Text\" /\u003e\u003c/p\u003e",
39
+ image: {
40
+ source: "https://dummyfileurl.com/file.png",
41
+ width: 500,
42
+ height: 500
43
+ },
44
+ areas: [
45
+ [
46
+ {
47
+ x: 44.259838136427874,
48
+ y: 43.53909783154884
49
+ },
50
+ {
51
+ x: 44.259838136427874,
52
+ y: 50.71315372424723
53
+ },
54
+ {
55
+ x: 52.070397807562195,
56
+ y: 50.71315372424723
57
+ },
58
+ {
59
+ x: 52.070397807562195,
60
+ y: 43.53909783154884
61
+ }
62
+ ]
63
+ ],
64
+ area_attributes: {
65
+ global: {
66
+ fill: "rgba(12, 176, 216, 0.48)",
67
+ stroke: "rgba(25, 90, 107, 0.83)"
68
+ }
69
+ },
70
+ validation: {
71
+ scoring_type: "exactMatch",
72
+ valid_response: {
73
+ score: 1.0,
74
+ value: ["0"]
75
+ }
76
+ },
77
+ is_math: true
78
+ })
79
+ end
80
+ end
81
+
82
+ context "when the hotspot type is oval" do
83
+ let(:file_name) { "hotspot_oval.json" }
84
+
85
+ it_behaves_like "handles common errors"
86
+
87
+ it "converts NQ JSON to hotspot item" do
88
+ conversion = subject.convert
89
+
90
+ expect(conversion[:question]).to eq({
91
+ type: "hotspot",
92
+ stimulus: "\u003cp\u003e\u003cem\u003e\u003cstrong\u003eThis\u003c/strong\u003e\u003c/em\u003e is a \u003cstrong\u003equestion\u003c/strong\u003e stem \u003cem\u003ewith\u003c/em\u003e some \u003cspan style=\"text-decoration: underline;\"\u003eadditional\u003c/span\u003e text to \u003cspan style=\"color: #ba372a;\"\u003etest \u003c/span\u003ewacky \u003cspan style=\"background-color: #fbeeb8;\"\u003eformatting\u003c/span\u003e\u003c/p\u003e\u003cp\u003e\\(234+\\alpha^4-56\\)\u003c/p\u003e\u003cp\u003eSome math is before this for \u003csub\u003etesting\u003c/sub\u003e\u003c/p\u003e\u003cp\u003e\u003cimg src=\"https://dummy-inscloudgate.net/files/3ec7226b-fd6d-49df-8236-cb52001ea10b?token=eyJ0eXAiOiJKV1QiLCJhbGciOi\" alt=\"Alternative Text\" /\u003e\u003c/p\u003e",
93
+ image: {
94
+ source: "https://dummyfileurl.com/file.png",
95
+ width: 500,
96
+ height: 500
97
+ },
98
+ areas: [
99
+ [
100
+ { x: 44.032765159525724, y: 29.48407951334979 },
101
+ { x: 42.4667627323164, y: 36.15418092808709 },
102
+ { x: 38.00716512438368, y: 41.80881986705052 },
103
+ { x: 31.33290564518235, y: 45.58712881121254 },
104
+ { x: 23.460079798107966, y: 46.91389447428033 },
105
+ { x: 15.587253951033581, y: 45.58712881121254 },
106
+ { x: 8.912994471832251, y: 41.80881986705052 },
107
+ { x: 4.453396863899535, y: 36.15418092808709 },
108
+ { x: 2.887394436690208, y: 29.484079513349794 },
109
+ { x: 4.453396863899535, y: 22.8139780986125 },
110
+ { x: 8.912994471832247, y: 17.159339159649065 },
111
+ { x: 15.58725395103357, y: 13.381030215487055 },
112
+ { x: 23.460079798107962, y: 12.054264552419248 },
113
+ { x: 31.332905645182358, y: 13.381030215487051 },
114
+ { x: 38.007165124383675, y: 17.159339159649065 },
115
+ { x: 42.46676273231639, y: 22.813978098612488 }
116
+ ]
117
+ ],
118
+ area_attributes: {
119
+ global: {
120
+ fill: "rgba(12, 176, 216, 0.48)", stroke: "rgba(25, 90, 107, 0.83)"
121
+ }
122
+ },
123
+ validation: {
124
+ scoring_type: "exactMatch",
125
+ valid_response: {
126
+ score: 1.0,
127
+ value: ["0"]
128
+ }
129
+ },
130
+ is_math: true
131
+ })
132
+
133
+ expect(conversion[:title]).to eq("Oval Type Question")
134
+ end
135
+ end
136
+
137
+ context "when the hotspot type is polygon" do
138
+ let(:file_name) { "hotspot_polygon.json" }
139
+
140
+ it_behaves_like "handles common errors"
141
+
142
+ it "converts NQ JSON to hotspot item" do
143
+ conversion = subject.convert
144
+
145
+ expect(conversion[:question]).to eq({
146
+ type: "hotspot",
147
+ stimulus: "\u003cp\u003e\u003cem\u003e\u003cstrong\u003eThis\u003c/strong\u003e\u003c/em\u003e is a \u003cstrong\u003equestion\u003c/strong\u003e stem \u003cem\u003ewith\u003c/em\u003e some \u003cspan style=\"text-decoration: underline;\"\u003eadditional\u003c/span\u003e text to \u003cspan style=\"color: #ba372a;\"\u003etest \u003c/span\u003ewacky \u003cspan style=\"background-color: #fbeeb8;\"\u003eformatting\u003c/span\u003e\u003c/p\u003e\u003cp\u003e\\(234+\\alpha^4-56\\)\u003c/p\u003e\u003cp\u003eSome math is before this for \u003csub\u003etesting\u003c/sub\u003e\u003c/p\u003e\u003cp\u003e\u003cimg src=\"https://dummy-inscloudgate.net/files/3ec7226b-fd6d-49df-8236-cb52001ea10b?token=eyJ0eXAiOiJKV1QiLCJhbGciOi\" alt=\"Alternative Text\" /\u003e\u003c/p\u003e",
148
+ image: {
149
+ source: "https://dummyfileurl.com/file.png",
150
+ width: 500,
151
+ height: 500
152
+ },
153
+ areas: [
154
+ [
155
+ { x: 9.384031919243187, y: 17.26691949400596 },
156
+ { x: 3.428780893569626, y: 23.294051770215578 },
157
+ { x: 1.082772913758829, y: 33.230675252615235 },
158
+ { x: 17.86575307702068, y: 34.696734454936504 },
159
+ { x: 36.814279067800186, y: 34.370943521087334 },
160
+ { x: 44.21322731181886, y: 20.19903789864848 },
161
+ { x: 28.3325579100227, y: 10.26241441624882 },
162
+ { x: 16.24159370638244, y: 14.49769655628802 },
163
+ { x: 9.384031919243187, y: 17.26691949400596 }
164
+ ]
165
+ ],
166
+ area_attributes: {
167
+ global: { fill: "rgba(12, 176, 216, 0.48)",
168
+ stroke: "rgba(25, 90, 107, 0.83)" }
169
+ },
170
+ validation: {
171
+ scoring_type: "exactMatch",
172
+ valid_response: {
173
+ score: 1.0,
174
+ value: ["0"]
175
+ }
176
+ },
177
+ is_math: true
178
+ })
179
+ expect(conversion[:title]).to eq("Polygon Question")
180
+ end
181
+ end
182
+ 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::NewQuiz::Matching do
9
+ let(:question) { File.read(File.join(SPEC_ROOT, "fixtures", "sample_data", "new_quiz", "matching.json")) }
10
+
11
+ subject { described_class.new(question) }
12
+
13
+ it_behaves_like "handles common errors"
14
+
15
+ it "converts to association item" do
16
+ conversion = subject.convert
17
+ expect(conversion[:question]).to eq({ possible_responses: ["This is a distractor", "4", "2", "1", "3"],
18
+ stimulus_list: %w[A B C D],
19
+ stimulus: "<p>Match the letter with its corresponding number. </p>" \
20
+ '<p><img src="https://dummy-inscloudgate.net/files/3ec7226b-fd6d-49df-8236-cb52001ea10b?token=eyJ0eXAiOiJKV1QiLCJhbGciOi" ' \
21
+ 'alt="Some alternative text about how this is a sink spitting fire" /></p>' \
22
+ '<p><em><strong>This</strong></em> is <span style="color: #236fa1;">some</span> <strong>extra</strong> text I ' \
23
+ '<span style="text-decoration: underline;">addded</span> to do <em>some</em> <span style="background-color: #236fa1;">funky</span> ' \
24
+ "<sub>formatting</sub> with :)</p><ul><li>this</li><li>is </li><li><strong>a</strong></li><li>list</li></ul>",
25
+ type: "association",
26
+ validation: { valid_response: { score: 2, value: %w[1 2 3 4] },
27
+ scoring_type: "partialMatchV2" },
28
+ metadata: { distractor_rationale_response_level: ["This is a distractor"] } })
29
+ end
30
+ end
@@ -0,0 +1,34 @@
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::MultipleAnswer do
9
+ let(:question) { File.read(File.join(SPEC_ROOT, "fixtures", "sample_data", "new_quiz", "multiple_answer.json")) }
10
+
11
+ subject { described_class.new(question) }
12
+
13
+ it_behaves_like "handles common errors"
14
+
15
+ it "converts to mcq item" do
16
+ conversion = subject.convert
17
+ expect(conversion[:question]).to eq({ stimulus: '<p>wewe</p><p><img src="https://dummy-inscloudgate.net/files/3ec7226b-fd6d-49df-8236-cb52001ea10b?token=eyJ0eXAiOiJKV1QiLCJhbGciOi" alt="Alternative Text" /></p>',
18
+ options: [{ label: "<p>c1</p>", value: "c74e82c2-433b-4ee3-89b1-ed70e5f7fdc0" },
19
+ { label: "<p>c2</p>",
20
+ value: "50430f5d-8001-4157-800f-f721bd89132a" },
21
+ { label: "<p>c3</p>",
22
+ value: "79a7fd67-8940-4f33-a0c2-e4117441449d" },
23
+ { label: "<p>c4</p>",
24
+ value: "148659e6-dc03-400a-bbd5-5ab5b8c3c1c2" }],
25
+ type: "mcq",
26
+ validation: {
27
+ valid_response: { score: 1,
28
+ value: %w[c74e82c2-433b-4ee3-89b1-ed70e5f7fdc0
29
+ 50430f5d-8001-4157-800f-f721bd89132a] },
30
+ scoring_type: "exactMatch"
31
+ },
32
+ multiple_responses: true })
33
+ end
34
+ end
@@ -0,0 +1,54 @@
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::MultipleChoice do
9
+ let(:question) { File.read(File.join(SPEC_ROOT, "fixtures", "sample_data", "new_quiz", "multiple_choice.json")) }
10
+
11
+ subject { described_class.new(question) }
12
+
13
+ it_behaves_like "handles common errors"
14
+
15
+ it "converts to mcq item" do
16
+ conversion = subject.convert
17
+ expect(conversion[:question]).to eq({ stimulus: '<p>This is <strong>stem</strong> with <span style="text-decoration: underline;">formatting<br /><br /></span></p><p><img src="https://dummy-inscloudgate.net/files/3ec7226b-fd6d-49df-8236-cb52001ea10b?token=eyJ0eXAiOiJKV1QiLCJhbGciOi" alt="Alternative Text" /></p>',
18
+ options: [{ label: "<p>a</p>", value: "c537d12a-863d-4d09-ab72-76443d011fc3" },
19
+ { label: "<p>b</p>",
20
+ value: "ef867920-4248-40ed-8407-824ad950b06a" },
21
+ { label: "<p>c</p>",
22
+ value: "62af8f8c-edbe-4fd4-963a-b440e5b4b15c" },
23
+ { label: "<p>d</p>",
24
+ value: "c97895b1-184e-4894-b3f0-5b6f14dca68d" }],
25
+ type: "mcq",
26
+ validation: { valid_response: { score: 3, value: ["c537d12a-863d-4d09-ab72-76443d011fc3"] },
27
+ scoring_type: "exactMatch" },
28
+ metadata: { distractor_rationale_response_level: ["<p>feedback a</p>",
29
+ "<p>feedback b</p>",
30
+ "<p>Feedback c</p>"] } })
31
+ end
32
+
33
+ context "contains math in the stimulus and options" do
34
+ let(:question) do
35
+ File.read(File.join(SPEC_ROOT, "fixtures", "sample_data", "new_quiz", "multiple_choice_math.json"))
36
+ end
37
+
38
+ it "converts math to LaTeX format" do
39
+ conversion = subject.convert
40
+ expect(conversion[:question]).to eq({ stimulus: '<p>Which of the following statements are not equivalent to the following?</p><p>\\(2x=4\\)<img src="https://dummy-inscloudgate.net/files/3ec7226b-fd6d-49df-8236-cb52001ea10b?token=eyJ0eXAiOiJKV1QiLCJhbGciOi" alt="Alternative Text" /></p>',
41
+ options: [{ label: '<p>\\(1x=2\\)</p>', value: "f6bfe582-afbd-4289-9a74-e581856c98ac" },
42
+ { label: '<p>\\(3x=6\\)</p>',
43
+ value: "289a9b39-f922-48c1-aabf-1a91f68ea0f3" },
44
+ { label: '<p>\\(4x=8\\)</p>',
45
+ value: "b9c8eb14-fea5-44c1-a049-4cf767c01c27" },
46
+ { label: '<p>\\(3x=3\\)</p>',
47
+ value: "2fe41298-b4b5-418c-ab5d-2604ff7b8a45" }],
48
+ type: "mcq",
49
+ validation: { valid_response: { score: 1, value: ["2fe41298-b4b5-418c-ab5d-2604ff7b8a45"] },
50
+ scoring_type: "exactMatch" },
51
+ is_math: true })
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,48 @@
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::Numerical do
9
+ let(:question) { File.read(File.join(SPEC_ROOT, "fixtures", "sample_data", "new_quiz", "numerical.json")) }
10
+
11
+ subject { described_class.new(question) }
12
+
13
+ it_behaves_like "handles common errors"
14
+
15
+ it "converts to clozeformula item" do
16
+ conversion = subject.convert
17
+ expect(conversion[:question]).to eq({ stimulus: '<p>This is a sample question for numerical type</p><img src="https://dummy-inscloudgate.net/files/3ec7226b-fd6d-49df-8236-cb52001ea10b?token=eyJ0eXAiOiJKV1QiLCJhbGciOi" alt="Alternative Text" />',
18
+ template: "{{response}}",
19
+ response_container: { template: "" },
20
+ type: "clozeformula",
21
+ validation: { scoring_type: "exactMatch",
22
+ valid_response: { score: 1,
23
+ value: [[{ method: "equivLiteral", value: "100", options: { ignoreOrder: false, inverseResult: false } },
24
+ { method: "equivValue",
25
+ value: '50.0\\pm10.0',
26
+ options: {
27
+ ignoreOrder: false, inverseResult: false
28
+ } },
29
+ { method: "equivValue",
30
+ value: '50.0\\pm15.0',
31
+ options: {
32
+ ignoreOrder: false, inverseResult: false
33
+ } },
34
+ { method: "equivValue",
35
+ value: '75.0\\pm25.0',
36
+ options: {
37
+ ignoreOrder: false, inverseResult: false
38
+ } },
39
+ { method: "equivValue",
40
+ value: "12.345",
41
+ options: { decimalPlaces: 3 } },
42
+ { method: "equivValue",
43
+ value: "3.14159",
44
+ options: { decimalPlaces: 5 } }]] } },
45
+ response_containers: [],
46
+ is_math: true })
47
+ end
48
+ 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::NewQuiz::Ordering do
9
+ let(:question) { File.read(File.join(SPEC_ROOT, "fixtures", "sample_data", "new_quiz", "ordering.json")) }
10
+
11
+ subject { described_class.new(question) }
12
+
13
+ it_behaves_like "handles common errors"
14
+
15
+ it "converts to ordering item" do
16
+ conversion = subject.convert
17
+ expect(conversion[:question]).to eq({ list: ["<p>3</p>", "<p>4</p>", "<p>2</p>", "<p>11</p>", "<p>0</p>"],
18
+ stimulus: '<p>Question <strong>stem</strong></p><p><img src="https://dummy-inscloudgate.net/files/3ec7226b-fd6d-49df-8236-cb52001ea10b?token=eyJ0eXAiOiJKV1QiLCJhbGciOi" alt="Alternative Text" /></p>',
19
+ type: "orderlist",
20
+ validation: { scoring_type: "exactMatch",
21
+ valid_response: { score: 1, value: [1, 0, 2, 3, 4] } },
22
+ ui_style: { type: "list" } })
23
+ expect(conversion[:title]).to eq("Order Quiz #2")
24
+ end
25
+ end