ams_migration 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +9 -0
- data/README.md +33 -0
- data/lib/ams_migration/canvas/converter/classic_quiz/calculated.rb +33 -0
- data/lib/ams_migration/canvas/converter/classic_quiz/classic_question.rb +114 -0
- data/lib/ams_migration/canvas/converter/classic_quiz/essay.rb +36 -0
- data/lib/ams_migration/canvas/converter/classic_quiz/file_upload.rb +35 -0
- data/lib/ams_migration/canvas/converter/classic_quiz/fill_in_blank.rb +63 -0
- data/lib/ams_migration/canvas/converter/classic_quiz/fill_in_multiple_blanks.rb +111 -0
- data/lib/ams_migration/canvas/converter/classic_quiz/matching.rb +66 -0
- data/lib/ams_migration/canvas/converter/classic_quiz/multiple_answer.rb +27 -0
- data/lib/ams_migration/canvas/converter/classic_quiz/multiple_choice.rb +57 -0
- data/lib/ams_migration/canvas/converter/classic_quiz/multiple_dropdown.rb +126 -0
- data/lib/ams_migration/canvas/converter/classic_quiz/numerical.rb +89 -0
- data/lib/ams_migration/canvas/converter/classic_quiz/text.rb +28 -0
- data/lib/ams_migration/canvas/converter/classic_quiz/true_false.rb +14 -0
- data/lib/ams_migration/canvas/converter/classic_quiz/unknown.rb +31 -0
- data/lib/ams_migration/canvas/converter/new_quiz/categorization.rb +108 -0
- data/lib/ams_migration/canvas/converter/new_quiz/essay.rb +78 -0
- data/lib/ams_migration/canvas/converter/new_quiz/file_upload.rb +33 -0
- data/lib/ams_migration/canvas/converter/new_quiz/fill_in_blank.rb +180 -0
- data/lib/ams_migration/canvas/converter/new_quiz/formula.rb +33 -0
- data/lib/ams_migration/canvas/converter/new_quiz/hotspot.rb +166 -0
- data/lib/ams_migration/canvas/converter/new_quiz/matching.rb +85 -0
- data/lib/ams_migration/canvas/converter/new_quiz/multiple_answer.rb +28 -0
- data/lib/ams_migration/canvas/converter/new_quiz/multiple_choice.rb +70 -0
- data/lib/ams_migration/canvas/converter/new_quiz/new_question.rb +133 -0
- data/lib/ams_migration/canvas/converter/new_quiz/numerical.rb +117 -0
- data/lib/ams_migration/canvas/converter/new_quiz/ordering.rb +68 -0
- data/lib/ams_migration/canvas/converter/new_quiz/stimulus.rb +35 -0
- data/lib/ams_migration/canvas/converter/new_quiz/true_false.rb +33 -0
- data/lib/ams_migration/canvas/converter/new_quiz/unknown.rb +30 -0
- data/lib/ams_migration/canvas/converter/question_type.rb +30 -0
- data/lib/ams_migration/services/README.md +26 -0
- data/lib/ams_migration/services/data_provider.rb +26 -0
- data/lib/ams_migration/services/learnosity_bank_cleaner.rb +44 -0
- data/lib/ams_migration/services/learnosity_data_api_client.rb +135 -0
- data/lib/ams_migration/services/learnosity_sink.rb +190 -0
- data/lib/ams_migration/services/migration_service.rb +37 -0
- data/lib/ams_migration/services/new_quiz_learnosity_transformer.rb +115 -0
- data/lib/ams_migration/version.rb +3 -0
- data/lib/ams_migration.rb +45 -0
- data/lib/factories/classic_quiz_item_converter_factory.rb +43 -0
- data/lib/factories/new_quiz_item_converter_factory.rb +76 -0
- data/spec/fixtures/sample_data/classic_quiz/calculated.json +24 -0
- data/spec/fixtures/sample_data/classic_quiz/essay.json +24 -0
- data/spec/fixtures/sample_data/classic_quiz/file_upload.json +24 -0
- data/spec/fixtures/sample_data/classic_quiz/fill_in_bank.json +24 -0
- data/spec/fixtures/sample_data/classic_quiz/fill_in_blank.json +53 -0
- data/spec/fixtures/sample_data/classic_quiz/fill_in_multiple_blanks.json +57 -0
- data/spec/fixtures/sample_data/classic_quiz/matching.json +107 -0
- data/spec/fixtures/sample_data/classic_quiz/multiple_answer.json +48 -0
- data/spec/fixtures/sample_data/classic_quiz/multiple_choice.json +56 -0
- data/spec/fixtures/sample_data/classic_quiz/multiple_dropdown.json +73 -0
- data/spec/fixtures/sample_data/classic_quiz/numerical.json +55 -0
- data/spec/fixtures/sample_data/classic_quiz/text.json +24 -0
- data/spec/fixtures/sample_data/classic_quiz/true_false.json +39 -0
- data/spec/fixtures/sample_data/classic_quiz/unknown.json +24 -0
- data/spec/fixtures/sample_data/new_quiz/bank_entry_item.json +5 -0
- data/spec/fixtures/sample_data/new_quiz/bank_item.json +11 -0
- data/spec/fixtures/sample_data/new_quiz/categorization.json +135 -0
- data/spec/fixtures/sample_data/new_quiz/essay.json +40 -0
- data/spec/fixtures/sample_data/new_quiz/file_upload.json +30 -0
- data/spec/fixtures/sample_data/new_quiz/fill_in_blank_association.json +162 -0
- data/spec/fixtures/sample_data/new_quiz/fill_in_blank_dropdown.json +180 -0
- data/spec/fixtures/sample_data/new_quiz/fill_in_blank_math.json +45 -0
- data/spec/fixtures/sample_data/new_quiz/fill_in_blank_mixed.json +188 -0
- data/spec/fixtures/sample_data/new_quiz/fill_in_blank_text.json +140 -0
- data/spec/fixtures/sample_data/new_quiz/formula.json +51 -0
- data/spec/fixtures/sample_data/new_quiz/hotspot_oval.json +38 -0
- data/spec/fixtures/sample_data/new_quiz/hotspot_polygon.json +66 -0
- data/spec/fixtures/sample_data/new_quiz/hotspot_square.json +38 -0
- data/spec/fixtures/sample_data/new_quiz/matching.json +88 -0
- data/spec/fixtures/sample_data/new_quiz/multiple_answer.json +57 -0
- data/spec/fixtures/sample_data/new_quiz/multiple_choice.json +66 -0
- data/spec/fixtures/sample_data/new_quiz/multiple_choice_math.json +55 -0
- data/spec/fixtures/sample_data/new_quiz/numerical.json +64 -0
- data/spec/fixtures/sample_data/new_quiz/ordering.json +60 -0
- data/spec/fixtures/sample_data/new_quiz/stimulus.json +17 -0
- data/spec/fixtures/sample_data/new_quiz/true_false.json +27 -0
- data/spec/fixtures/sample_data/new_quiz/unknown.json +14 -0
- data/spec/lib/ams_migration/services/learnosity_sink_spec.rb +198 -0
- data/spec/lib/canvas/converter/classic_quiz/calculated_spec.rb +26 -0
- data/spec/lib/canvas/converter/classic_quiz/essay_spec.rb +26 -0
- data/spec/lib/canvas/converter/classic_quiz/file_upload_spec.rb +23 -0
- data/spec/lib/canvas/converter/classic_quiz/fill_in_blank_spec.rb +30 -0
- data/spec/lib/canvas/converter/classic_quiz/fill_in_multiple_blanks_spec.rb +33 -0
- data/spec/lib/canvas/converter/classic_quiz/matching_spec.rb +38 -0
- data/spec/lib/canvas/converter/classic_quiz/multiple_answer_spec.rb +33 -0
- data/spec/lib/canvas/converter/classic_quiz/multiple_choice_spec.rb +31 -0
- data/spec/lib/canvas/converter/classic_quiz/multiple_dropdown_spec.rb +39 -0
- data/spec/lib/canvas/converter/classic_quiz/numerical_spec.rb +41 -0
- data/spec/lib/canvas/converter/classic_quiz/text_spec.rb +20 -0
- data/spec/lib/canvas/converter/classic_quiz/true_false_spec.rb +28 -0
- data/spec/lib/canvas/converter/classic_quiz/unknown_spec.rb +25 -0
- data/spec/lib/canvas/converter/common_error.rb +16 -0
- data/spec/lib/canvas/converter/new_quiz/categorization_spec.rb +55 -0
- data/spec/lib/canvas/converter/new_quiz/essay_spec.rb +26 -0
- data/spec/lib/canvas/converter/new_quiz/file_upload_spec.rb +24 -0
- data/spec/lib/canvas/converter/new_quiz/fill_in_blank_spec.rb +111 -0
- data/spec/lib/canvas/converter/new_quiz/formula_spec.rb +23 -0
- data/spec/lib/canvas/converter/new_quiz/hotspot_spec.rb +182 -0
- data/spec/lib/canvas/converter/new_quiz/matching_spec.rb +30 -0
- data/spec/lib/canvas/converter/new_quiz/multiple_answer_spec.rb +34 -0
- data/spec/lib/canvas/converter/new_quiz/multiple_choice_spec.rb +54 -0
- data/spec/lib/canvas/converter/new_quiz/numerical_spec.rb +48 -0
- data/spec/lib/canvas/converter/new_quiz/ordering_spec.rb +25 -0
- data/spec/lib/canvas/converter/new_quiz/stimulus_spec.rb +20 -0
- data/spec/lib/canvas/converter/new_quiz/true_false_spec.rb +24 -0
- data/spec/lib/canvas/converter/new_quiz/unknown_spec.rb +36 -0
- data/spec/lib/factories/classic_quiz_item_converter_factory_spec.rb +183 -0
- data/spec/lib/factories/new_quiz_item_converter_factory_spec.rb +71 -0
- data/spec/lib/services/learnosity_data_api_client_spec.rb +130 -0
- data/spec/lib/services/new_quiz_learnosity_transformer_spec.rb +196 -0
- data/spec/spec_helper.rb +20 -0
- metadata +229 -0
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
|