atomic_assessments_import 0.1.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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +56 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE +21 -0
- data/README.md +110 -0
- data/Rakefile +8 -0
- data/lib/atomic_assessments_import/csv/converter.rb +146 -0
- data/lib/atomic_assessments_import/csv/questions/multiple_choice.rb +104 -0
- data/lib/atomic_assessments_import/csv/questions/question.rb +86 -0
- data/lib/atomic_assessments_import/csv/utils.rb +25 -0
- data/lib/atomic_assessments_import/csv.rb +8 -0
- data/lib/atomic_assessments_import/export.rb +23 -0
- data/lib/atomic_assessments_import/version.rb +5 -0
- data/lib/atomic_assessments_import/writer.rb +24 -0
- data/lib/atomic_assessments_import.rb +34 -0
- metadata +114 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 4da5264007a4f90b2beb7c2237855d31229aac3e7266d8c2c6a47a30d364177b
|
4
|
+
data.tar.gz: 2d592d63a61a4888c5df9e4c374d68083b91c8db0fbef079330af3417a440912
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f7ee30d1484fbf993757311a0b76f2ddcfa2aa3dffc64b2bfb1ff42e5a02693cf27dad1c42f684bd8659955b8a5306024440ce462a99d37f8dceeb139dcb5d28
|
7
|
+
data.tar.gz: 514f3ac7e9d57e41cbcac462c3f8d55593e282584ddd827c1851d74008cc4549ef2271e83afa83c76e7a9babc6f867f2815d634bb76ca6f0db5350b4451d4741
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require:
|
2
|
+
- rubocop-rspec
|
3
|
+
|
4
|
+
AllCops:
|
5
|
+
TargetRubyVersion: 3.3
|
6
|
+
|
7
|
+
Style/TrailingCommaInArguments:
|
8
|
+
Description: 'Checks for trailing comma in argument lists.'
|
9
|
+
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-array-commas'
|
10
|
+
EnforcedStyleForMultiline: comma
|
11
|
+
Enabled: false
|
12
|
+
|
13
|
+
Style/TrailingCommaInArrayLiteral:
|
14
|
+
Description: 'Checks for trailing comma in array literals.'
|
15
|
+
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-array-commas'
|
16
|
+
EnforcedStyleForMultiline: comma
|
17
|
+
Enabled: true
|
18
|
+
|
19
|
+
Style/TrailingCommaInHashLiteral:
|
20
|
+
Description: 'Checks for trailing comma in hash literals.'
|
21
|
+
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-array-commas'
|
22
|
+
EnforcedStyleForMultiline: comma
|
23
|
+
Enabled: false
|
24
|
+
|
25
|
+
Metrics/BlockLength:
|
26
|
+
Enabled: false
|
27
|
+
|
28
|
+
RSpec/ExampleLength:
|
29
|
+
Enabled: false
|
30
|
+
|
31
|
+
Style/StringLiterals:
|
32
|
+
Description: 'Checks if uses of quotes match the configured preference.'
|
33
|
+
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#consistent-string-literals'
|
34
|
+
EnforcedStyle: double_quotes
|
35
|
+
Enabled: true
|
36
|
+
|
37
|
+
Metrics/AbcSize:
|
38
|
+
Max: 50
|
39
|
+
|
40
|
+
Metrics/CyclomaticComplexity:
|
41
|
+
Max: 20
|
42
|
+
|
43
|
+
Metrics/MethodLength:
|
44
|
+
Max: 50
|
45
|
+
|
46
|
+
Metrics/ClassLength:
|
47
|
+
Max: 200
|
48
|
+
|
49
|
+
RSpec/MultipleExpectations:
|
50
|
+
Max: 16
|
51
|
+
|
52
|
+
Style/Documentation:
|
53
|
+
Enabled: false
|
54
|
+
|
55
|
+
Style/NegatedIf:
|
56
|
+
Enabled: false
|
data/CHANGELOG.md
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 Atomic Jolt
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
# AtomicAssessmentsImports
|
2
|
+
|
3
|
+
Import converters for atomic assessments
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Install the gem and add to the application's Gemfile by executing:
|
8
|
+
|
9
|
+
$ bundle add atomic_assessments_imports
|
10
|
+
|
11
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
12
|
+
|
13
|
+
$ gem install atomic_assessments_imports
|
14
|
+
|
15
|
+
## Conversion scripts
|
16
|
+
|
17
|
+
Convert a CSV to a learnosity archive:
|
18
|
+
|
19
|
+
$ bin/convert input.csv output.zip
|
20
|
+
|
21
|
+
Convert a CSV to json:
|
22
|
+
|
23
|
+
$ bin/convert_to_json input.csv
|
24
|
+
|
25
|
+
## CSV input format
|
26
|
+
|
27
|
+
CSV Columns:
|
28
|
+
|
29
|
+
### Question ID
|
30
|
+
External question id. Importing the same question ID twice will overwrite previous imports
|
31
|
+
### Title
|
32
|
+
Item title
|
33
|
+
### Tag: tag_type
|
34
|
+
Entries in this column represent tag names in type "tag_type". This column can be repeated any number of times with multiple tag types.
|
35
|
+
### Question Text
|
36
|
+
Question stem
|
37
|
+
### Question Type
|
38
|
+
Question type. One of:
|
39
|
+
- Multiple choice (default)
|
40
|
+
### Template
|
41
|
+
Question type template. One of:
|
42
|
+
- Standard (default)
|
43
|
+
- Block layout
|
44
|
+
- Multiple response
|
45
|
+
- Block layout multiple response
|
46
|
+
- Choice matrix
|
47
|
+
- Choice matrix inline
|
48
|
+
- Choice matrix labels
|
49
|
+
"Standard" and "Block layout" are single response question types. The other templates are multiple response.
|
50
|
+
### Correct Answer
|
51
|
+
Correct response option, e.g. "A"
|
52
|
+
For multiple response questions, use a semicolon separator. e.g. "A;C;D"
|
53
|
+
### Option A
|
54
|
+
Text for option A
|
55
|
+
### Option B
|
56
|
+
Text for option B
|
57
|
+
### Option C through Option O
|
58
|
+
Text for subsequent options
|
59
|
+
### Option A Feedback
|
60
|
+
Feedback for option A
|
61
|
+
### Option B Feedback
|
62
|
+
Feedback for option B
|
63
|
+
### Option C Feedback through Option O Feedback
|
64
|
+
Feedback for subsequent options
|
65
|
+
### Scoring Type
|
66
|
+
Learnosity scoring type. One of:
|
67
|
+
- Partial Match Per Response (default)
|
68
|
+
- Partial Match
|
69
|
+
- Exact Match
|
70
|
+
### Shuffle options
|
71
|
+
Whether to shuffle answers. One of:
|
72
|
+
- Yes
|
73
|
+
- No (default)
|
74
|
+
### Points
|
75
|
+
Question points (default 1)
|
76
|
+
### General Feedback
|
77
|
+
General feedback
|
78
|
+
### Correct Feedback
|
79
|
+
Correct feedback
|
80
|
+
### Partially Correct Feedback
|
81
|
+
Partially correct feedback
|
82
|
+
### Incorrect Feedback
|
83
|
+
Incorrect feedback
|
84
|
+
### Distractor Rationale
|
85
|
+
Distractor rationale feedback
|
86
|
+
### Stimulus review
|
87
|
+
Stimulus (review only)
|
88
|
+
### Acknowledgements
|
89
|
+
Acknowledgements
|
90
|
+
### Instructor stimulus
|
91
|
+
Instructor stimulus
|
92
|
+
### Sample Answer
|
93
|
+
Sample answer
|
94
|
+
### Description
|
95
|
+
Item description
|
96
|
+
### Alignment URL
|
97
|
+
URL used to generate standard alignment tags. This column can be repeated.
|
98
|
+
|
99
|
+
|
100
|
+
|
101
|
+
|
102
|
+
## Development
|
103
|
+
|
104
|
+
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.
|
105
|
+
|
106
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
107
|
+
|
108
|
+
## Contributing
|
109
|
+
|
110
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/atomicjolt/atomic_assessments_imports.
|
data/Rakefile
ADDED
@@ -0,0 +1,146 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "byebug"
|
4
|
+
require "csv"
|
5
|
+
require "active_support/core_ext/digest/uuid"
|
6
|
+
|
7
|
+
require_relative "questions/question"
|
8
|
+
require_relative "questions/multiple_choice"
|
9
|
+
require_relative "utils"
|
10
|
+
|
11
|
+
module AtomicAssessmentsImport
|
12
|
+
module CSV
|
13
|
+
class Converter
|
14
|
+
HEADERS = [
|
15
|
+
"question id",
|
16
|
+
"title",
|
17
|
+
/tag: *[a-zA-Z0-9_].*/,
|
18
|
+
"question text",
|
19
|
+
"question type",
|
20
|
+
"template",
|
21
|
+
"correct answer",
|
22
|
+
/option [a-o]/,
|
23
|
+
/option [a-o] feedback/,
|
24
|
+
"scoring type",
|
25
|
+
"shuffle options",
|
26
|
+
"points",
|
27
|
+
"general feedback",
|
28
|
+
"correct feedback",
|
29
|
+
"partially correct feedback",
|
30
|
+
"incorrect feedback",
|
31
|
+
"distractor rationale",
|
32
|
+
"stimulus review",
|
33
|
+
"acknowledgements",
|
34
|
+
"instructor stimulus",
|
35
|
+
"sample answer",
|
36
|
+
"description",
|
37
|
+
"alignment url",
|
38
|
+
].freeze
|
39
|
+
|
40
|
+
def initialize(file)
|
41
|
+
@file = file
|
42
|
+
end
|
43
|
+
|
44
|
+
def convert
|
45
|
+
items = []
|
46
|
+
questions = []
|
47
|
+
|
48
|
+
::CSV.foreach(
|
49
|
+
@file,
|
50
|
+
headers: true,
|
51
|
+
header_converters: lambda do |header|
|
52
|
+
normalized = header.strip
|
53
|
+
normalized =
|
54
|
+
if (m = normalized.match(/^tag:(.+)$/i))
|
55
|
+
"tag:#{m[1].strip}"
|
56
|
+
else
|
57
|
+
normalized.downcase
|
58
|
+
end
|
59
|
+
if !HEADERS.find { |h| h.is_a?(Regexp) ? h =~ normalized : h == normalized }
|
60
|
+
raise ArgumentError, "Unknown column: #{header}"
|
61
|
+
end
|
62
|
+
|
63
|
+
normalized
|
64
|
+
end,
|
65
|
+
converters: ->(data) { data&.strip }
|
66
|
+
) do |row|
|
67
|
+
item, question_widgets = convert_row(row)
|
68
|
+
|
69
|
+
items << item
|
70
|
+
questions += question_widgets
|
71
|
+
rescue StandardError => e
|
72
|
+
raise e, "Error processing row: #{row.inspect} - #{e.message}"
|
73
|
+
end
|
74
|
+
|
75
|
+
{
|
76
|
+
activities: [],
|
77
|
+
items:,
|
78
|
+
questions:,
|
79
|
+
features: [],
|
80
|
+
errors: [],
|
81
|
+
}
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def tags(row)
|
87
|
+
tags = {}
|
88
|
+
row.headers.each.with_index do |header, idx|
|
89
|
+
if header&.start_with?("tag:")
|
90
|
+
tag_name = header.gsub(/^tag:/, "").to_sym
|
91
|
+
tags[tag_name] ||= []
|
92
|
+
tags[tag_name] << row[idx] if row[idx].present?
|
93
|
+
end
|
94
|
+
end
|
95
|
+
if alignment_urls(row).present?
|
96
|
+
tags[:lrn_aligned] = alignment_urls(row).map do |url|
|
97
|
+
Digest::UUID.uuid_v5(Digest::UUID::URL_NAMESPACE, url)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
tags
|
101
|
+
end
|
102
|
+
|
103
|
+
def alignment_urls(row)
|
104
|
+
if row.headers.include?("alignment url")
|
105
|
+
row.headers.filter_map.with_index do |header, idx|
|
106
|
+
row[idx] if header == "alignment url" && row[idx].present?
|
107
|
+
end
|
108
|
+
end.presence
|
109
|
+
end
|
110
|
+
|
111
|
+
def convert_row(row)
|
112
|
+
question = Questions::Question.load(row)
|
113
|
+
item = {
|
114
|
+
reference: SecureRandom.uuid,
|
115
|
+
title: row["title"] || "",
|
116
|
+
status: "published",
|
117
|
+
tags: tags(row),
|
118
|
+
metadata: {
|
119
|
+
import_date: Time.now.iso8601,
|
120
|
+
**{
|
121
|
+
question_id: row["question id"],
|
122
|
+
alignment: alignment_urls(row),
|
123
|
+
}.compact,
|
124
|
+
},
|
125
|
+
description: row["description"] || "",
|
126
|
+
questions: [
|
127
|
+
{
|
128
|
+
reference: question.reference,
|
129
|
+
type: question.question_type,
|
130
|
+
},
|
131
|
+
],
|
132
|
+
features: [],
|
133
|
+
definition: {
|
134
|
+
widgets: [
|
135
|
+
{
|
136
|
+
reference: question.reference,
|
137
|
+
widget_type: "response",
|
138
|
+
},
|
139
|
+
]
|
140
|
+
},
|
141
|
+
}
|
142
|
+
[item, [question.to_learnosity]]
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,104 @@
|
|
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 ArgumentError, "Missing correct answer" if correct_responses.empty?
|
17
|
+
raise ArgumentError, "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"
|
96
|
+
{ type: "horizontal" }
|
97
|
+
else
|
98
|
+
raise NotImplementedError, "Unknown template"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,86 @@
|
|
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 NotImplementedError, "Unknown 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 NotImplementedError, "Unknown 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
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "byebug"
|
4
|
+
require "csv"
|
5
|
+
require "active_support/core_ext/digest/uuid"
|
6
|
+
|
7
|
+
require_relative "questions/question"
|
8
|
+
require_relative "questions/multiple_choice"
|
9
|
+
|
10
|
+
module AtomicAssessmentsImport
|
11
|
+
module CSV
|
12
|
+
module Utils
|
13
|
+
def self.parse_boolean(value, default:)
|
14
|
+
case value&.downcase
|
15
|
+
when "true", "yes", "y", "1"
|
16
|
+
true
|
17
|
+
when "false", "no", "n", "0"
|
18
|
+
false
|
19
|
+
else
|
20
|
+
default
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AtomicAssessmentsImport
|
4
|
+
module Export
|
5
|
+
def self.create(path, data)
|
6
|
+
AtomicAssessmentsImport::Writer.new(path).open do |writer|
|
7
|
+
writer.write("export.json", { version: 2.0 }.to_json)
|
8
|
+
|
9
|
+
data[:activities].each do |activity|
|
10
|
+
writer.write("activities/#{activity[:reference]}.json", activity.to_json)
|
11
|
+
end
|
12
|
+
|
13
|
+
data[:questions].each do |question|
|
14
|
+
writer.write("questions/#{question[:reference]}.json", question.to_json)
|
15
|
+
end
|
16
|
+
|
17
|
+
data[:items].each do |item|
|
18
|
+
writer.write("items/#{item[:reference]}.json", item.to_json)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "zip"
|
4
|
+
|
5
|
+
module AtomicAssessmentsImport
|
6
|
+
class Writer
|
7
|
+
def initialize(path)
|
8
|
+
@path = path
|
9
|
+
end
|
10
|
+
|
11
|
+
def open
|
12
|
+
@zip = Zip::File.open(@path, Zip::File::CREATE)
|
13
|
+
yield self
|
14
|
+
ensure
|
15
|
+
@zip.close
|
16
|
+
end
|
17
|
+
|
18
|
+
def write(filename, content)
|
19
|
+
raise "Zip file is not open" unless @zip
|
20
|
+
|
21
|
+
@zip.get_output_stream(filename) { |file| file.write(content) }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/all"
|
4
|
+
require "mimemagic"
|
5
|
+
require_relative "atomic_assessments_import/version"
|
6
|
+
require_relative "atomic_assessments_import/csv"
|
7
|
+
require_relative "atomic_assessments_import/writer"
|
8
|
+
require_relative "atomic_assessments_import/export"
|
9
|
+
|
10
|
+
module AtomicAssessmentsImport
|
11
|
+
class Error < StandardError; end
|
12
|
+
|
13
|
+
def self.convert(path)
|
14
|
+
type = MimeMagic.by_path(path)&.type
|
15
|
+
|
16
|
+
converter =
|
17
|
+
case type
|
18
|
+
when "text/csv"
|
19
|
+
CSV::Converter.new(path)
|
20
|
+
else
|
21
|
+
raise ArgumentError, "Unsupported file type"
|
22
|
+
end
|
23
|
+
|
24
|
+
converter.convert
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.convert_to_aa_format(input_path, output_path)
|
28
|
+
result = convert(input_path)
|
29
|
+
AtomicAssessmentsImport::Export.create(output_path, result)
|
30
|
+
{
|
31
|
+
errors: result[:errors],
|
32
|
+
}
|
33
|
+
end
|
34
|
+
end
|
metadata
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: atomic_assessments_import
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Sean Collings
|
8
|
+
- Matt Petro
|
9
|
+
autorequire:
|
10
|
+
bindir: exe
|
11
|
+
cert_chain: []
|
12
|
+
date: 2025-02-27 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: activesupport
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - ">="
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '0'
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '0'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: csv
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - ">="
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '0'
|
35
|
+
type: :runtime
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: mimemagic
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0'
|
49
|
+
type: :runtime
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: rubyzip
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
type: :runtime
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
description: Importer to Convert different formats to AA's import format
|
71
|
+
email:
|
72
|
+
- support@atomicjolt.com
|
73
|
+
executables: []
|
74
|
+
extensions: []
|
75
|
+
extra_rdoc_files: []
|
76
|
+
files:
|
77
|
+
- ".rspec"
|
78
|
+
- ".rubocop.yml"
|
79
|
+
- CHANGELOG.md
|
80
|
+
- LICENSE
|
81
|
+
- README.md
|
82
|
+
- Rakefile
|
83
|
+
- lib/atomic_assessments_import.rb
|
84
|
+
- lib/atomic_assessments_import/csv.rb
|
85
|
+
- 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
|
89
|
+
- lib/atomic_assessments_import/export.rb
|
90
|
+
- lib/atomic_assessments_import/version.rb
|
91
|
+
- lib/atomic_assessments_import/writer.rb
|
92
|
+
homepage: https://github.com/atomicjolt/atomic_assessments_import
|
93
|
+
licenses: []
|
94
|
+
metadata: {}
|
95
|
+
post_install_message:
|
96
|
+
rdoc_options: []
|
97
|
+
require_paths:
|
98
|
+
- lib
|
99
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 3.3.0
|
104
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
105
|
+
requirements:
|
106
|
+
- - ">="
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
version: '0'
|
109
|
+
requirements: []
|
110
|
+
rubygems_version: 3.5.11
|
111
|
+
signing_key:
|
112
|
+
specification_version: 4
|
113
|
+
summary: Importer to Convert different formats to AA's import format
|
114
|
+
test_files: []
|