atomic_assessments_import 0.3.0 → 0.5.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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +22 -1
  3. data/docs/plans/2026-02-11-flexible-examsoft-importer-design.md +127 -0
  4. data/docs/plans/2026-02-11-flexible-examsoft-importer-plan.md +2635 -0
  5. data/lib/atomic_assessments_import/csv/converter.rb +3 -3
  6. data/lib/atomic_assessments_import/exam_soft/chunker/heading_split_strategy.rb +38 -0
  7. data/lib/atomic_assessments_import/exam_soft/chunker/horizontal_rule_split_strategy.rb +37 -0
  8. data/lib/atomic_assessments_import/exam_soft/chunker/metadata_marker_strategy.rb +38 -0
  9. data/lib/atomic_assessments_import/exam_soft/chunker/numbered_question_strategy.rb +41 -0
  10. data/lib/atomic_assessments_import/exam_soft/chunker/strategy.rb +22 -0
  11. data/lib/atomic_assessments_import/exam_soft/chunker.rb +46 -0
  12. data/lib/atomic_assessments_import/exam_soft/converter.rb +230 -0
  13. data/lib/atomic_assessments_import/exam_soft/extractor/correct_answer_detector.rb +36 -0
  14. data/lib/atomic_assessments_import/exam_soft/extractor/feedback_detector.rb +50 -0
  15. data/lib/atomic_assessments_import/exam_soft/extractor/metadata_detector.rb +37 -0
  16. data/lib/atomic_assessments_import/exam_soft/extractor/options_detector.rb +44 -0
  17. data/lib/atomic_assessments_import/exam_soft/extractor/question_stem_detector.rb +44 -0
  18. data/lib/atomic_assessments_import/exam_soft/extractor/question_type_detector.rb +51 -0
  19. data/lib/atomic_assessments_import/exam_soft/extractor.rb +95 -0
  20. data/lib/atomic_assessments_import/exam_soft.rb +10 -0
  21. data/lib/atomic_assessments_import/questions/cloze_dropdown.rb +62 -0
  22. data/lib/atomic_assessments_import/questions/essay.rb +20 -0
  23. data/lib/atomic_assessments_import/questions/fill_in_the_blank.rb +49 -0
  24. data/lib/atomic_assessments_import/questions/matching.rb +42 -0
  25. data/lib/atomic_assessments_import/questions/multiple_choice.rb +102 -0
  26. data/lib/atomic_assessments_import/questions/ordering.rb +53 -0
  27. data/lib/atomic_assessments_import/questions/question.rb +106 -0
  28. data/lib/atomic_assessments_import/questions/short_answer.rb +24 -0
  29. data/lib/atomic_assessments_import/utils.rb +21 -0
  30. data/lib/atomic_assessments_import/version.rb +1 -1
  31. data/lib/atomic_assessments_import.rb +30 -12
  32. metadata +43 -5
  33. data/lib/atomic_assessments_import/csv/questions/multiple_choice.rb +0 -104
  34. data/lib/atomic_assessments_import/csv/questions/question.rb +0 -86
  35. data/lib/atomic_assessments_import/csv/utils.rb +0 -24
@@ -7,26 +7,44 @@ require_relative "atomic_assessments_import/version"
7
7
  require_relative "atomic_assessments_import/csv"
8
8
  require_relative "atomic_assessments_import/writer"
9
9
  require_relative "atomic_assessments_import/export"
10
+ require_relative "atomic_assessments_import/exam_soft"
10
11
 
11
12
  module AtomicAssessmentsImport
12
13
  class Error < StandardError; end
13
14
 
14
- def self.convert(path)
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)
15
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
16
28
 
17
- converter =
18
- case type
19
- when "text/csv"
20
- CSV::Converter.new(path)
21
- else
22
- raise "Unsupported file type"
23
- 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)
24
35
 
25
- converter.convert
26
- end
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)
27
44
 
28
- def self.convert_to_aa_format(input_path, output_path)
29
- result = convert(input_path)
45
+
46
+ def self.convert_to_aa_format(input_path, output_path, import_from: nil)
47
+ result = convert(input_path, import_from)
30
48
  AtomicAssessmentsImport::Export.create(output_path, result)
31
49
  {
32
50
  errors: result[:errors],
metadata CHANGED
@@ -1,11 +1,12 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: atomic_assessments_import
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sean Collings
8
8
  - Matt Petro
9
+ - Jacob Schwartz
9
10
  bindir: exe
10
11
  cert_chain: []
11
12
  date: 1980-01-02 00:00:00.000000000 Z
@@ -80,6 +81,20 @@ dependencies:
80
81
  - - "~>"
81
82
  - !ruby/object:Gem::Version
82
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'
83
98
  description: Importer to Convert different formats to AA's import format
84
99
  email:
85
100
  - support@atomicjolt.com
@@ -93,13 +108,36 @@ files:
93
108
  - LICENSE
94
109
  - README.md
95
110
  - Rakefile
111
+ - docs/plans/2026-02-11-flexible-examsoft-importer-design.md
112
+ - docs/plans/2026-02-11-flexible-examsoft-importer-plan.md
96
113
  - lib/atomic_assessments_import.rb
97
114
  - lib/atomic_assessments_import/csv.rb
98
115
  - lib/atomic_assessments_import/csv/converter.rb
99
- - lib/atomic_assessments_import/csv/questions/multiple_choice.rb
100
- - lib/atomic_assessments_import/csv/questions/question.rb
101
- - lib/atomic_assessments_import/csv/utils.rb
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
102
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
103
141
  - lib/atomic_assessments_import/version.rb
104
142
  - lib/atomic_assessments_import/writer.rb
105
143
  homepage: https://github.com/atomicjolt/atomic_assessments_import
@@ -119,7 +157,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
119
157
  - !ruby/object:Gem::Version
120
158
  version: '0'
121
159
  requirements: []
122
- rubygems_version: 3.7.1
160
+ rubygems_version: 3.6.7
123
161
  specification_version: 4
124
162
  summary: Importer to Convert different formats to AA's import format
125
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