atomic_assessments_import 0.2.4 → 0.4.0
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 +4 -4
- data/README.md +21 -1
- data/docs/plans/2026-02-11-flexible-examsoft-importer-design.md +127 -0
- data/docs/plans/2026-02-11-flexible-examsoft-importer-plan.md +2635 -0
- data/lib/atomic_assessments_import/csv/converter.rb +3 -3
- data/lib/atomic_assessments_import/exam_soft/chunker/heading_split_strategy.rb +38 -0
- data/lib/atomic_assessments_import/exam_soft/chunker/horizontal_rule_split_strategy.rb +37 -0
- data/lib/atomic_assessments_import/exam_soft/chunker/metadata_marker_strategy.rb +38 -0
- data/lib/atomic_assessments_import/exam_soft/chunker/numbered_question_strategy.rb +41 -0
- data/lib/atomic_assessments_import/exam_soft/chunker/strategy.rb +22 -0
- data/lib/atomic_assessments_import/exam_soft/chunker.rb +46 -0
- data/lib/atomic_assessments_import/exam_soft/converter.rb +203 -0
- data/lib/atomic_assessments_import/exam_soft/extractor/correct_answer_detector.rb +36 -0
- data/lib/atomic_assessments_import/exam_soft/extractor/feedback_detector.rb +50 -0
- data/lib/atomic_assessments_import/exam_soft/extractor/metadata_detector.rb +37 -0
- data/lib/atomic_assessments_import/exam_soft/extractor/options_detector.rb +44 -0
- data/lib/atomic_assessments_import/exam_soft/extractor/question_stem_detector.rb +44 -0
- data/lib/atomic_assessments_import/exam_soft/extractor/question_type_detector.rb +51 -0
- data/lib/atomic_assessments_import/exam_soft/extractor.rb +96 -0
- data/lib/atomic_assessments_import/exam_soft.rb +10 -0
- data/lib/atomic_assessments_import/questions/cloze_dropdown.rb +62 -0
- data/lib/atomic_assessments_import/questions/essay.rb +20 -0
- data/lib/atomic_assessments_import/questions/fill_in_the_blank.rb +49 -0
- data/lib/atomic_assessments_import/questions/matching.rb +42 -0
- data/lib/atomic_assessments_import/questions/multiple_choice.rb +102 -0
- data/lib/atomic_assessments_import/questions/ordering.rb +53 -0
- data/lib/atomic_assessments_import/questions/question.rb +106 -0
- data/lib/atomic_assessments_import/questions/short_answer.rb +24 -0
- data/lib/atomic_assessments_import/utils.rb +21 -0
- data/lib/atomic_assessments_import/version.rb +1 -1
- data/lib/atomic_assessments_import/writer.rb +1 -1
- data/lib/atomic_assessments_import.rb +31 -12
- metadata +62 -13
- data/lib/atomic_assessments_import/csv/questions/multiple_choice.rb +0 -104
- data/lib/atomic_assessments_import/csv/questions/question.rb +0 -86
- data/lib/atomic_assessments_import/csv/utils.rb +0 -24
|
@@ -2,30 +2,49 @@
|
|
|
2
2
|
|
|
3
3
|
require "active_support/all"
|
|
4
4
|
require "mimemagic"
|
|
5
|
+
require "tempfile"
|
|
5
6
|
require_relative "atomic_assessments_import/version"
|
|
6
7
|
require_relative "atomic_assessments_import/csv"
|
|
7
8
|
require_relative "atomic_assessments_import/writer"
|
|
8
9
|
require_relative "atomic_assessments_import/export"
|
|
10
|
+
require_relative "atomic_assessments_import/exam_soft"
|
|
9
11
|
|
|
10
12
|
module AtomicAssessmentsImport
|
|
11
13
|
class Error < StandardError; end
|
|
12
14
|
|
|
13
|
-
def self.
|
|
15
|
+
def self.register_converter(mime_type, source, klass)
|
|
16
|
+
@converters ||= {}
|
|
17
|
+
@converters[[mime_type, source]] = klass
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.convert(path, import_from)
|
|
14
21
|
type = MimeMagic.by_path(path)&.type
|
|
22
|
+
converter_class = @converters[[type, import_from]]
|
|
23
|
+
|
|
24
|
+
raise "Unsupported file type: #{type} from #{import_from == nil ? "Unspecified Source" : import_from}. Make sure the file type conversion is available for the specified source." unless converter_class
|
|
25
|
+
|
|
26
|
+
converter_class.new(path).convert
|
|
27
|
+
end
|
|
15
28
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
end
|
|
29
|
+
######################
|
|
30
|
+
# Register converters: format is register_converter(mime_type, source, class)
|
|
31
|
+
######################
|
|
32
|
+
# CSV converter - csv is the original/default importer so it can be used with either source specified as "csv" or with no source specified
|
|
33
|
+
register_converter("text/csv", "csv", CSV::Converter)
|
|
34
|
+
register_converter("text/csv", nil, CSV::Converter)
|
|
23
35
|
|
|
24
|
-
|
|
25
|
-
|
|
36
|
+
# ExamSoft converters
|
|
37
|
+
## rtf
|
|
38
|
+
register_converter("application/rtf", "examsoft", ExamSoft::Converter)
|
|
39
|
+
## docx
|
|
40
|
+
register_converter("application/vnd.openxmlformats-officedocument.wordprocessingml.document", "examsoft", ExamSoft::Converter)
|
|
41
|
+
## html
|
|
42
|
+
register_converter("text/html", "examsoft", ExamSoft::Converter)
|
|
43
|
+
register_converter("application/xhtml+xml", "examsoft", ExamSoft::Converter)
|
|
26
44
|
|
|
27
|
-
|
|
28
|
-
|
|
45
|
+
|
|
46
|
+
def self.convert_to_aa_format(input_path, output_path, import_from: nil)
|
|
47
|
+
result = convert(input_path, import_from)
|
|
29
48
|
AtomicAssessmentsImport::Export.create(output_path, result)
|
|
30
49
|
{
|
|
31
50
|
errors: result[:errors],
|
metadata
CHANGED
|
@@ -1,16 +1,30 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: atomic_assessments_import
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sean Collings
|
|
8
8
|
- Matt Petro
|
|
9
|
-
|
|
9
|
+
- Jacob Schwartz
|
|
10
10
|
bindir: exe
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date:
|
|
12
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
|
+
- !ruby/object:Gem::Dependency
|
|
15
|
+
name: byebug
|
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
|
17
|
+
requirements:
|
|
18
|
+
- - ">="
|
|
19
|
+
- !ruby/object:Gem::Version
|
|
20
|
+
version: '0'
|
|
21
|
+
type: :development
|
|
22
|
+
prerelease: false
|
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
24
|
+
requirements:
|
|
25
|
+
- - ">="
|
|
26
|
+
- !ruby/object:Gem::Version
|
|
27
|
+
version: '0'
|
|
14
28
|
- !ruby/object:Gem::Dependency
|
|
15
29
|
name: activesupport
|
|
16
30
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -57,16 +71,30 @@ dependencies:
|
|
|
57
71
|
name: rubyzip
|
|
58
72
|
requirement: !ruby/object:Gem::Requirement
|
|
59
73
|
requirements:
|
|
60
|
-
- - "
|
|
74
|
+
- - "~>"
|
|
61
75
|
- !ruby/object:Gem::Version
|
|
62
|
-
version: '0'
|
|
76
|
+
version: '3.0'
|
|
63
77
|
type: :runtime
|
|
64
78
|
prerelease: false
|
|
65
79
|
version_requirements: !ruby/object:Gem::Requirement
|
|
66
80
|
requirements:
|
|
67
|
-
- - "
|
|
81
|
+
- - "~>"
|
|
68
82
|
- !ruby/object:Gem::Version
|
|
69
|
-
version: '0'
|
|
83
|
+
version: '3.0'
|
|
84
|
+
- !ruby/object:Gem::Dependency
|
|
85
|
+
name: pandoc-ruby
|
|
86
|
+
requirement: !ruby/object:Gem::Requirement
|
|
87
|
+
requirements:
|
|
88
|
+
- - "~>"
|
|
89
|
+
- !ruby/object:Gem::Version
|
|
90
|
+
version: '2.1'
|
|
91
|
+
type: :runtime
|
|
92
|
+
prerelease: false
|
|
93
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
94
|
+
requirements:
|
|
95
|
+
- - "~>"
|
|
96
|
+
- !ruby/object:Gem::Version
|
|
97
|
+
version: '2.1'
|
|
70
98
|
description: Importer to Convert different formats to AA's import format
|
|
71
99
|
email:
|
|
72
100
|
- support@atomicjolt.com
|
|
@@ -80,19 +108,41 @@ files:
|
|
|
80
108
|
- LICENSE
|
|
81
109
|
- README.md
|
|
82
110
|
- Rakefile
|
|
111
|
+
- docs/plans/2026-02-11-flexible-examsoft-importer-design.md
|
|
112
|
+
- docs/plans/2026-02-11-flexible-examsoft-importer-plan.md
|
|
83
113
|
- lib/atomic_assessments_import.rb
|
|
84
114
|
- lib/atomic_assessments_import/csv.rb
|
|
85
115
|
- lib/atomic_assessments_import/csv/converter.rb
|
|
86
|
-
- lib/atomic_assessments_import/
|
|
87
|
-
- lib/atomic_assessments_import/
|
|
88
|
-
- lib/atomic_assessments_import/
|
|
116
|
+
- lib/atomic_assessments_import/exam_soft.rb
|
|
117
|
+
- lib/atomic_assessments_import/exam_soft/chunker.rb
|
|
118
|
+
- lib/atomic_assessments_import/exam_soft/chunker/heading_split_strategy.rb
|
|
119
|
+
- lib/atomic_assessments_import/exam_soft/chunker/horizontal_rule_split_strategy.rb
|
|
120
|
+
- lib/atomic_assessments_import/exam_soft/chunker/metadata_marker_strategy.rb
|
|
121
|
+
- lib/atomic_assessments_import/exam_soft/chunker/numbered_question_strategy.rb
|
|
122
|
+
- lib/atomic_assessments_import/exam_soft/chunker/strategy.rb
|
|
123
|
+
- lib/atomic_assessments_import/exam_soft/converter.rb
|
|
124
|
+
- lib/atomic_assessments_import/exam_soft/extractor.rb
|
|
125
|
+
- lib/atomic_assessments_import/exam_soft/extractor/correct_answer_detector.rb
|
|
126
|
+
- lib/atomic_assessments_import/exam_soft/extractor/feedback_detector.rb
|
|
127
|
+
- lib/atomic_assessments_import/exam_soft/extractor/metadata_detector.rb
|
|
128
|
+
- lib/atomic_assessments_import/exam_soft/extractor/options_detector.rb
|
|
129
|
+
- lib/atomic_assessments_import/exam_soft/extractor/question_stem_detector.rb
|
|
130
|
+
- lib/atomic_assessments_import/exam_soft/extractor/question_type_detector.rb
|
|
89
131
|
- lib/atomic_assessments_import/export.rb
|
|
132
|
+
- lib/atomic_assessments_import/questions/cloze_dropdown.rb
|
|
133
|
+
- lib/atomic_assessments_import/questions/essay.rb
|
|
134
|
+
- lib/atomic_assessments_import/questions/fill_in_the_blank.rb
|
|
135
|
+
- lib/atomic_assessments_import/questions/matching.rb
|
|
136
|
+
- lib/atomic_assessments_import/questions/multiple_choice.rb
|
|
137
|
+
- lib/atomic_assessments_import/questions/ordering.rb
|
|
138
|
+
- lib/atomic_assessments_import/questions/question.rb
|
|
139
|
+
- lib/atomic_assessments_import/questions/short_answer.rb
|
|
140
|
+
- lib/atomic_assessments_import/utils.rb
|
|
90
141
|
- lib/atomic_assessments_import/version.rb
|
|
91
142
|
- lib/atomic_assessments_import/writer.rb
|
|
92
143
|
homepage: https://github.com/atomicjolt/atomic_assessments_import
|
|
93
144
|
licenses: []
|
|
94
145
|
metadata: {}
|
|
95
|
-
post_install_message:
|
|
96
146
|
rdoc_options: []
|
|
97
147
|
require_paths:
|
|
98
148
|
- lib
|
|
@@ -107,8 +157,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
107
157
|
- !ruby/object:Gem::Version
|
|
108
158
|
version: '0'
|
|
109
159
|
requirements: []
|
|
110
|
-
rubygems_version: 3.
|
|
111
|
-
signing_key:
|
|
160
|
+
rubygems_version: 3.6.7
|
|
112
161
|
specification_version: 4
|
|
113
162
|
summary: Importer to Convert different formats to AA's import format
|
|
114
163
|
test_files: []
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "question"
|
|
4
|
-
|
|
5
|
-
module AtomicAssessmentsImport
|
|
6
|
-
module CSV
|
|
7
|
-
module Questions
|
|
8
|
-
class MultipleChoice < Question
|
|
9
|
-
QUESTION_INDEXES = ("a".."o").to_a.freeze
|
|
10
|
-
|
|
11
|
-
def question_type
|
|
12
|
-
"mcq"
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def question_data
|
|
16
|
-
raise "Missing correct answer" if correct_responses.empty?
|
|
17
|
-
raise "Missing options" if options.empty?
|
|
18
|
-
|
|
19
|
-
super.deep_merge(
|
|
20
|
-
{
|
|
21
|
-
multiple_responses: multiple_responses,
|
|
22
|
-
options: options,
|
|
23
|
-
validation: {
|
|
24
|
-
scoring_type: scoring_type,
|
|
25
|
-
valid_response: {
|
|
26
|
-
score: points,
|
|
27
|
-
value: correct_responses,
|
|
28
|
-
},
|
|
29
|
-
rounding: "none",
|
|
30
|
-
penalty: 1,
|
|
31
|
-
},
|
|
32
|
-
shuffle_options: Utils.parse_boolean(@row["shuffle options"], default: false),
|
|
33
|
-
ui_style: ui_style,
|
|
34
|
-
}
|
|
35
|
-
)
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def metadata
|
|
39
|
-
super.merge(
|
|
40
|
-
{
|
|
41
|
-
distractor_rationale_response_level: distractor_rationale_response_level,
|
|
42
|
-
}
|
|
43
|
-
)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def options
|
|
47
|
-
QUESTION_INDEXES.filter_map.with_index do |value, cnt|
|
|
48
|
-
key = "option #{value}"
|
|
49
|
-
if @row[key].present?
|
|
50
|
-
{
|
|
51
|
-
label: @row[key],
|
|
52
|
-
value: cnt.to_s,
|
|
53
|
-
}
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def correct_responses
|
|
59
|
-
correct = @row["correct answer"]&.split(";")&.map(&:strip)&.map(&:downcase) || []
|
|
60
|
-
|
|
61
|
-
correct.filter_map do |value|
|
|
62
|
-
QUESTION_INDEXES.find_index(value).to_s
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def distractor_rationale_response_level
|
|
67
|
-
QUESTION_INDEXES.map do |value|
|
|
68
|
-
key = "option #{value} feedback"
|
|
69
|
-
@row[key].presence || ""
|
|
70
|
-
end.reverse.drop_while(&:blank?).reverse
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def multiple_responses
|
|
74
|
-
case @row["template"]&.downcase
|
|
75
|
-
when "multiple response", "block layout multiple response", "choice matrix",
|
|
76
|
-
"choice matrix inline", "choice matrix labels"
|
|
77
|
-
true
|
|
78
|
-
else
|
|
79
|
-
false
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def ui_style
|
|
84
|
-
case @row["template"]&.downcase
|
|
85
|
-
when "multiple response"
|
|
86
|
-
{ type: "horizontal" }
|
|
87
|
-
when "block layout", "block layout multiple response"
|
|
88
|
-
{ choice_label: "upper-alpha", type: "block" }
|
|
89
|
-
when "choice matrix"
|
|
90
|
-
{ horizontal_lines: false, type: "table" }
|
|
91
|
-
when "choice matrix inline"
|
|
92
|
-
{ horizontal_lines: false, type: "inline" }
|
|
93
|
-
when "choice matrix labels"
|
|
94
|
-
{ stem_numeration: "upper-alpha", horizontal_lines: false, type: "table" }
|
|
95
|
-
when nil, "", "multiple choice", "standard"
|
|
96
|
-
{ type: "horizontal" }
|
|
97
|
-
else
|
|
98
|
-
raise "Unknown template: #{@row["template"]}"
|
|
99
|
-
end
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
end
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module AtomicAssessmentsImport
|
|
4
|
-
module CSV
|
|
5
|
-
module Questions
|
|
6
|
-
class Question
|
|
7
|
-
def initialize(row)
|
|
8
|
-
@row = row
|
|
9
|
-
# @question_reference = Digest::UUID.uuid_v5(Digest::UUID::URL_NAMESPACE, "#{@item_url}/question")
|
|
10
|
-
@reference = SecureRandom.uuid
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def self.load(row)
|
|
14
|
-
case row["question type"]
|
|
15
|
-
when nil, "", /multiple choice/i, /mcq/i
|
|
16
|
-
MultipleChoice.new(row)
|
|
17
|
-
else
|
|
18
|
-
raise "Unknown question type #{row['question type']}"
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
attr_reader :reference
|
|
23
|
-
|
|
24
|
-
def question_type
|
|
25
|
-
raise NotImplementedError
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def question_data
|
|
29
|
-
{
|
|
30
|
-
stimulus: @row["question text"],
|
|
31
|
-
type: question_type,
|
|
32
|
-
metadata: metadata,
|
|
33
|
-
**{
|
|
34
|
-
stimulus_review: @row["stimulus review"],
|
|
35
|
-
instructor_stimulus: @row["instructor stimulus"],
|
|
36
|
-
}.compact,
|
|
37
|
-
}
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def metadata
|
|
41
|
-
{
|
|
42
|
-
distractor_rationale: @row["distractor rationale"],
|
|
43
|
-
sample_answer: @row["sample answer"],
|
|
44
|
-
acknowledgements: @row["acknowledgements"],
|
|
45
|
-
general_feedback: @row["general feedback"],
|
|
46
|
-
correct_feedback: @row["correct feedback"],
|
|
47
|
-
partially_correct_feedback: @row["partially correct feedback"],
|
|
48
|
-
incorrect_feedback: @row["incorrect feedback"],
|
|
49
|
-
}.compact
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def scoring_type
|
|
53
|
-
case @row["scoring type"]
|
|
54
|
-
when nil, "", /Partial Match Per Response/i
|
|
55
|
-
"partialMatchV2"
|
|
56
|
-
when /Partial Match/i
|
|
57
|
-
"partialMatch"
|
|
58
|
-
when /Exact Match/i
|
|
59
|
-
"exactMatch"
|
|
60
|
-
else
|
|
61
|
-
raise "Unknown scoring type #{@row['scoring type']}"
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def points
|
|
66
|
-
if @row["points"].blank?
|
|
67
|
-
1
|
|
68
|
-
else
|
|
69
|
-
Float(@row["points"])
|
|
70
|
-
end
|
|
71
|
-
rescue ArgumentError
|
|
72
|
-
1
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def to_learnosity
|
|
76
|
-
{
|
|
77
|
-
type: question_type,
|
|
78
|
-
widget_type: "response",
|
|
79
|
-
reference: @reference,
|
|
80
|
-
data: question_data,
|
|
81
|
-
}
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
end
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "csv"
|
|
4
|
-
require "active_support/core_ext/digest/uuid"
|
|
5
|
-
|
|
6
|
-
require_relative "questions/question"
|
|
7
|
-
require_relative "questions/multiple_choice"
|
|
8
|
-
|
|
9
|
-
module AtomicAssessmentsImport
|
|
10
|
-
module CSV
|
|
11
|
-
module Utils
|
|
12
|
-
def self.parse_boolean(value, default:)
|
|
13
|
-
case value&.downcase
|
|
14
|
-
when "true", "yes", "y", "1"
|
|
15
|
-
true
|
|
16
|
-
when "false", "no", "n", "0"
|
|
17
|
-
false
|
|
18
|
-
else
|
|
19
|
-
default
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|