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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +21 -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 +203 -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 +96 -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/writer.rb +1 -1
  32. data/lib/atomic_assessments_import.rb +31 -12
  33. metadata +62 -13
  34. data/lib/atomic_assessments_import/csv/questions/multiple_choice.rb +0 -104
  35. data/lib/atomic_assessments_import/csv/questions/question.rb +0 -86
  36. 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.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)
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
- converter =
17
- case type
18
- when "text/csv"
19
- CSV::Converter.new(path)
20
- else
21
- raise "Unsupported file type"
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
- converter.convert
25
- 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)
26
44
 
27
- def self.convert_to_aa_format(input_path, output_path)
28
- 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)
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.2.4
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sean Collings
8
8
  - Matt Petro
9
- autorequire:
9
+ - Jacob Schwartz
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2025-04-18 00:00:00.000000000 Z
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/csv/questions/multiple_choice.rb
87
- - lib/atomic_assessments_import/csv/questions/question.rb
88
- - 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
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.5.11
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