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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1bf85b2c570c6df0426d11cf34c77a9e4dbe5ba464168e98f339f9bd440f93a7
4
+ data.tar.gz: dd39b8350b3a7925ebc5cb91a8ec296c964a381d12526f559d13822f104e3647
5
+ SHA512:
6
+ metadata.gz: 7e768993b323305cdc1df07fb4f8635fe6c9871e0e43e4bfbf9930e95d0cc0b58c09935d4a6ce1e002722116e6dae800ffda728cea8458c1aa23ce3061640b09
7
+ data.tar.gz: 9fa668be4c9bf06ce1472469aa12c7258d8357f5d423c5f463cc33122f229c1d1471f6a87e0a179c7fed2f9fbbbdf73a49ce0fc3e7cc136605f1fa0114a1ef79
data/LICENSE.txt ADDED
@@ -0,0 +1,9 @@
1
+ Proprietary License – Limited Educational Use
2
+
3
+ Copyright (C) 2025–present Instructure, Inc.
4
+
5
+ This code is made publicly available for educational and reference purposes only.
6
+ You may not copy, modify, distribute, or use this code in any software or service
7
+ without express written permission from Instructure, Inc.
8
+
9
+ All rights reserved.
data/README.md ADDED
@@ -0,0 +1,33 @@
1
+ # AMS Migration
2
+
3
+ A collection of services to help with data migration tasks.
4
+
5
+ NOTE: The code for the services was copied from the MasteryConnect and slightly adjusted. Refer to [MasteryConnect](https://github.com/instructure/mc-mothership) for the git history.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'ams_migration'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle install
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install ams_migration
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Development
28
+
29
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
+
31
+ ## Contributing
32
+
33
+ Bug reports and pull requests are welcome.
@@ -0,0 +1,33 @@
1
+ require_relative "../question_type"
2
+
3
+ module AmsMigration
4
+ module Canvas
5
+ module Converter
6
+ module ClassicQuiz
7
+ class Calculated < ClassicQuestion
8
+ # This question type will be skipped in the conversion process and the
9
+ # question will not be included in the quiz. The JSON returned will be
10
+ # an essay question with text noting the question has been skipped.
11
+ def convert
12
+ super
13
+ {
14
+ question: {
15
+ 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.",
16
+ type: QuestionType::UNSUPPORTED_CANVAS_TYPE,
17
+ validation: {
18
+ valid_response: {
19
+ score: 0,
20
+ value: ""
21
+ },
22
+ scoring_type: "exactMatch"
23
+ },
24
+ is_math: true
25
+ },
26
+ errors: @conversion_errors
27
+ }
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,114 @@
1
+ # Copyright (C) 2025 - present Instructure, Inc.
2
+ # This code is made publicly available for educational and reference purposes only.
3
+ # You may not copy, modify, redistribute, or use this code in any software or service without explicit permission.
4
+
5
+ module AmsMigration
6
+ module Canvas
7
+ module Converter
8
+ module ClassicQuiz
9
+ class ClassicQuestion
10
+ MULTI_CHOICE = "multiple_choice_question".freeze
11
+ TRUE_FALSE = "true_false_question".freeze
12
+ ESSAY = "essay_question".freeze
13
+ FILL_IN_BLANK = "short_answer_question".freeze
14
+ FILL_IN_MULTIPLE_BLANKS = "fill_in_multiple_blanks_question".freeze
15
+ MULTIPLE_ANSWERS = "multiple_answers_question".freeze
16
+ MULTIPLE_DROPDOWNS = "multiple_dropdowns_question".freeze
17
+ MATCHING = "matching_question".freeze
18
+ NUMERICAL = "numerical_question".freeze
19
+ TEXT_ONLY = "text_only_question".freeze
20
+ CALCULATED = "calculated_question".freeze
21
+ FILE_UPLOAD = "file_upload_question".freeze
22
+
23
+ attr_reader :notifications, :image_sources
24
+
25
+ def initialize(question)
26
+ @question_json = question
27
+ @question = nil
28
+ @converted_question = {}
29
+ @conversion_errors = []
30
+ @notifications = []
31
+ @image_sources = []
32
+ end
33
+
34
+ def title
35
+ @question[:question_name] || ""
36
+ end
37
+
38
+ def stimulus
39
+ @question[:question_text]
40
+ end
41
+
42
+ def convert
43
+ begin
44
+ @question = JSON.parse(@question_json).deep_symbolize_keys
45
+ rescue JSON::ParserError => e
46
+ @question = ""
47
+ @conversion_errors << e.message
48
+ end
49
+ {
50
+ question: @converted_question,
51
+ errors: @conversion_errors
52
+ }
53
+ end
54
+
55
+ def point_value
56
+ point_value = @question[:points_possible] || 0
57
+ if point_value % 1 != 0
58
+ @notifications << "Partial points are not supported. After conversion points have been rounded to the nearest whole number."
59
+ end
60
+ point_value.round(0)
61
+ end
62
+
63
+ def post_convert
64
+ process_question_text
65
+ end
66
+
67
+ def process_question_text
68
+ if @converted_question.is_a?(Hash)
69
+ if @converted_question.key?(:stimulus)
70
+ @converted_question[:stimulus] = substitute_mathml(@converted_question[:stimulus])
71
+ update_image_sources(@converted_question[:stimulus])
72
+ end
73
+ if @converted_question.key?(:template)
74
+ @converted_question[:template] = substitute_mathml(@converted_question[:template])
75
+ update_image_sources(@converted_question[:template])
76
+ end
77
+
78
+ if @converted_question.key?(:options)
79
+ @converted_question[:options].each do |option|
80
+ option[:label] = substitute_mathml(option[:label])
81
+ update_image_sources(option[:label])
82
+ option[:value] = substitute_mathml(option[:value])
83
+ update_image_sources(option[:value])
84
+ end
85
+ end
86
+ else
87
+ @converted_question = substitute_mathml(@converted_question) unless @converted_question.nil?
88
+ update_image_sources(@converted_question) unless @converted_question.nil?
89
+ end
90
+ end
91
+
92
+ def substitute_mathml(fragment)
93
+ html = Nokogiri::HTML::DocumentFragment.parse(fragment, "UTF-8")
94
+ html.css("img.equation_image[x-canvaslms-safe-mathml]").each do |img|
95
+ img.replace(img.attr("x-canvaslms-safe-mathml"))
96
+ end
97
+ html.to_xhtml(indent: 0).delete("\n")
98
+ end
99
+
100
+ def update_image_sources(fragment)
101
+ html = Nokogiri::HTML::DocumentFragment.parse(fragment, "UTF-8")
102
+ html.css("img[data-api-endpoint]").each do |img|
103
+ @image_sources << img.attr("src")
104
+ end
105
+ end
106
+
107
+ def to_h
108
+ @question
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+ 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
+ module AmsMigration
6
+ module Canvas
7
+ module Converter
8
+ module ClassicQuiz
9
+ class Essay < ClassicQuestion
10
+ def convert
11
+ super
12
+ begin
13
+ @converted_question =
14
+ {
15
+ stimulus:,
16
+ type: "longtextV2",
17
+ metadata: {
18
+ rubric: "Rubric",
19
+ rubric_points: point_value
20
+ },
21
+ is_math: true
22
+ }
23
+ rescue => e
24
+ @conversion_errors << e.message
25
+ end
26
+ post_convert
27
+ {
28
+ question: @converted_question,
29
+ errors: @conversion_errors
30
+ }
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,35 @@
1
+ # Copyright (C) 2025 - present Instructure, Inc.
2
+ # This code is made publicly available for educational and reference purposes only.
3
+ # You may not copy, modify, redistribute, or use this code in any software or service without explicit permission.
4
+
5
+ module AmsMigration
6
+ module Canvas
7
+ module Converter
8
+ module ClassicQuiz
9
+ class FileUpload < ClassicQuestion
10
+ # This question type will be skipped in the conversion process and the
11
+ # question will not be included in the quiz. The JSON returned will be
12
+ # an essay question with text noting the question has been skipped.
13
+ def convert
14
+ super
15
+ {
16
+ question: {
17
+ 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: QuestionType::UNSUPPORTED_CANVAS_TYPE,
19
+ validation: {
20
+ valid_response: {
21
+ score: 0,
22
+ value: ""
23
+ },
24
+ scoring_type: "exactMatch"
25
+ },
26
+ is_math: true
27
+ },
28
+ errors: @conversion_errors
29
+ }
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,63 @@
1
+ module AmsMigration
2
+ module Canvas
3
+ module Converter
4
+ module ClassicQuiz
5
+ class FillInBlank < ClassicQuestion
6
+ def correct_answer
7
+ @question[:answers][0][:text] ||= ""
8
+ end
9
+
10
+ def alternate_responses
11
+ @question[:answers].drop(1).map do |answer|
12
+ {
13
+ score: (answer[:weight] == 100) ? point_value : 0,
14
+ value: answer[:text] ||= ""
15
+ }
16
+ end
17
+ end
18
+
19
+ def distractors
20
+ distractors = []
21
+ has_distractors = false
22
+ @question[:answers].each do |answer|
23
+ distractor = answer[:comments_html] ||= ""
24
+ distractors << distractor
25
+ has_distractors = true if distractor != ""
26
+ end
27
+ has_distractors ? distractors : nil
28
+ end
29
+
30
+ def convert
31
+ super
32
+ begin
33
+ @converted_question = {
34
+ template: "#{stimulus}<br/>{{response}}",
35
+ type: "clozetext",
36
+ validation: {
37
+ valid_response: {
38
+ score: point_value,
39
+ value: [correct_answer]
40
+ },
41
+ scoring_type: "exactMatch"
42
+ },
43
+ is_math: true
44
+ }
45
+
46
+ # distractors?
47
+ @converted_question[:metadata] = {
48
+ distractor_rationale_response_level: distractors
49
+ }
50
+ rescue => e
51
+ @conversion_errors << e.message
52
+ end
53
+ post_convert
54
+ {
55
+ question: @converted_question,
56
+ errors: @conversion_errors
57
+ }
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ 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
+ module AmsMigration
6
+ module Canvas
7
+ module Converter
8
+ module ClassicQuiz
9
+ class FillInMultipleBlanks < ClassicQuestion
10
+ def correct_answer
11
+ valid_values = {}
12
+ alternate_values = {}
13
+ ordered_templates = []
14
+ stimulus.scan(/\[.*?\]/).each_with_index do |template, _index|
15
+ template = template.delete("[").delete("]")
16
+ ordered_templates << template
17
+ @question[:answers].select { |answer| answer[:blank_id] == template }.each_with_index do |answer, index|
18
+ if index.zero? && answer[:text] && answer[:blank_id]
19
+ if valid_values[answer[:blank_id]]
20
+ valid_values[answer[:blank_id]] << answer[:text]
21
+ else
22
+ valid_values[answer[:blank_id]] = [answer[:text]]
23
+ end
24
+ elsif answer[:text] && answer[:blank_id]
25
+ if alternate_values[answer[:blank_id]]
26
+ alternate_values[answer[:blank_id]] << answer[:text]
27
+ else
28
+ alternate_values[answer[:blank_id]] = [answer[:text]]
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ valid_values.values.map(&:size).max
35
+
36
+ correct_values = ordered_templates.map do |template|
37
+ valid_values[template].first
38
+ end
39
+
40
+ valid_response = {
41
+ score: point_value,
42
+ value: correct_values
43
+ }
44
+
45
+ alternate_responses = []
46
+ max_answers = alternate_values.values.map(&:size).max
47
+ if max_answers
48
+ (0..(max_answers - 1)).each do |i|
49
+ alternate_response = ordered_templates.filter_map do |template|
50
+ alternate_values&.dig(template, i)
51
+ end
52
+ alternate_responses << {
53
+ score: 1,
54
+ value: alternate_response
55
+ }
56
+ end
57
+ end
58
+ {
59
+ valid_response:,
60
+ alternate_responses:
61
+ }
62
+ end
63
+
64
+ def distractors
65
+ result = []
66
+ has_distractors = false
67
+ @question[:answers].each do |answer|
68
+ distractor = answer[:comments_html] ||= ""
69
+ result << distractor
70
+ has_distractors = true if distractor != ""
71
+ end
72
+ has_distractors ? result : nil
73
+ end
74
+
75
+ def convert
76
+ super
77
+ begin
78
+ correct_values = correct_answer
79
+
80
+ @converted_question = {
81
+ template: stimulus.gsub("[", "{{response}}").delete("]").to_s,
82
+ type: "clozetext",
83
+ validation: {
84
+ valid_response: correct_values[:valid_response],
85
+ scoring_type: "exactMatch",
86
+ match_all_possible_responses: true
87
+ },
88
+ is_math: true
89
+ }
90
+
91
+ if correct_values[:alternate_responses].size.positive?
92
+ @converted_question[:validation][:alt_responses] = correct_values[:alternate_responses]
93
+ end
94
+
95
+ @converted_question[:metadata] = {
96
+ distractor_rationale_response_level: distractors
97
+ }
98
+ rescue => e
99
+ @conversion_errors << e.message
100
+ end
101
+ post_convert
102
+ {
103
+ question: @converted_question,
104
+ errors: @conversion_errors
105
+ }
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,66 @@
1
+ # Copyright (C) 2025 - present Instructure, Inc.
2
+ # This code is made publicly available for educational and reference purposes only.
3
+ # You may not copy, modify, redistribute, or use this code in any software or service without explicit permission.
4
+
5
+ module AmsMigration
6
+ module Canvas
7
+ module Converter
8
+ module ClassicQuiz
9
+ class Matching < ClassicQuestion
10
+ def option_response_pairs
11
+ return @option_response_pairs if defined? @option_response_pairs
12
+
13
+ stimulus_list = []
14
+ possible_responses = []
15
+ correct_values = []
16
+ @question[:answers].each do |answer|
17
+ stimulus_list << answer[:text] ||= ""
18
+ @question[:matches].each do |match|
19
+ correct_values << match[:text] if answer[:match_id] == match[:match_id]
20
+ end
21
+ end
22
+
23
+ possible_responses += correct_values
24
+ if @question[:matching_answer_incorrect_matches].present?
25
+ incorrect_values = @question[:matching_answer_incorrect_matches].split("\n")
26
+ possible_responses += incorrect_values
27
+ end
28
+
29
+ @option_response_pairs = {
30
+ stimulus_list:,
31
+ possible_responses:,
32
+ correct_values:
33
+ }
34
+ end
35
+
36
+ def convert
37
+ super
38
+ begin
39
+ @converted_question = {
40
+ stimulus:,
41
+ possible_responses: option_response_pairs[:possible_responses],
42
+ stimulus_list: option_response_pairs[:stimulus_list],
43
+ type: "association",
44
+ validation: {
45
+ valid_response: {
46
+ score: point_value,
47
+ value: option_response_pairs[:correct_values]
48
+ },
49
+ scoring_type: "exactMatch"
50
+ },
51
+ is_math: true
52
+ }
53
+ rescue => e
54
+ @conversion_errors << e.message
55
+ end
56
+ post_convert
57
+ {
58
+ question: @converted_question,
59
+ errors: @conversion_errors
60
+ }
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,27 @@
1
+ # Copyright (C) 2025 - present Instructure, Inc.
2
+ # This code is made publicly available for educational and reference purposes only.
3
+ # You may not copy, modify, redistribute, or use this code in any software or service without explicit permission.
4
+
5
+ module AmsMigration
6
+ module Canvas
7
+ module Converter
8
+ module ClassicQuiz
9
+ class MultipleAnswer < MultipleChoice
10
+ def convert
11
+ begin
12
+ multiple_answer = super
13
+ multiple_answer[:question][:multiple_responses] = true if multiple_answer[:errors].empty?
14
+ rescue => e
15
+ @conversion_errors << e.message
16
+ end
17
+ post_convert
18
+ {
19
+ question: @converted_question,
20
+ errors: @conversion_errors
21
+ }
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,57 @@
1
+ module AmsMigration
2
+ module Canvas
3
+ module Converter
4
+ module ClassicQuiz
5
+ class MultipleChoice < ClassicQuestion
6
+ def to_h
7
+ @question
8
+ end
9
+
10
+ def options
11
+ @question[:answers].map do |answer|
12
+ label = answer[:text]
13
+ # html content will take precedence if present
14
+ label = answer[:html] if answer[:html].present?
15
+ {
16
+ label:,
17
+ value: answer[:id].to_s
18
+ }
19
+ end
20
+ end
21
+
22
+ def correct_answer
23
+ @question[:answers].filter_map do |answer|
24
+ answer[:id].to_s if answer[:weight] == 100
25
+ end
26
+ end
27
+
28
+ def convert
29
+ super
30
+ begin
31
+ @converted_question = {
32
+ stimulus:,
33
+ options:,
34
+ type: "mcq",
35
+ validation: {
36
+ valid_response: {
37
+ score: point_value,
38
+ value: correct_answer
39
+ },
40
+ scoring_type: "exactMatch"
41
+ },
42
+ is_math: true
43
+ }
44
+ rescue => e
45
+ @conversion_errors << e.message
46
+ end
47
+ post_convert
48
+ {
49
+ question: @converted_question,
50
+ errors: @conversion_errors
51
+ }
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end