topographer 0.0.7 → 0.0.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/topographer/exceptions.rb +5 -3
- data/lib/topographer/importer.rb +25 -9
- data/lib/topographer/importer/helpers.rb +19 -13
- data/lib/topographer/importer/helpers/write_log_to_csv.rb +75 -67
- data/lib/topographer/importer/importable.rb +7 -3
- data/lib/topographer/importer/input.rb +11 -4
- data/lib/topographer/importer/input/base.rb +21 -15
- data/lib/topographer/importer/input/delimited_spreadsheet.rb +55 -0
- data/lib/topographer/importer/input/roo.rb +31 -24
- data/lib/topographer/importer/input/source_data.rb +14 -8
- data/lib/topographer/importer/logger.rb +10 -6
- data/lib/topographer/importer/logger/base.rb +75 -66
- data/lib/topographer/importer/logger/fatal_error_entry.rb +22 -16
- data/lib/topographer/importer/logger/log_entry.rb +31 -25
- data/lib/topographer/importer/logger/simple.rb +25 -19
- data/lib/topographer/importer/mapper.rb +58 -53
- data/lib/topographer/importer/mapper/default_field_mapping.rb +23 -17
- data/lib/topographer/importer/mapper/field_mapping.rb +56 -49
- data/lib/topographer/importer/mapper/ignored_field_mapping.rb +14 -7
- data/lib/topographer/importer/mapper/mapper_builder.rb +57 -44
- data/lib/topographer/importer/mapper/mapping_columns.rb +51 -44
- data/lib/topographer/importer/mapper/mapping_validator.rb +39 -32
- data/lib/topographer/importer/mapper/result.rb +21 -15
- data/lib/topographer/importer/mapper/validation_field_mapping.rb +35 -28
- data/lib/topographer/importer/strategy.rb +12 -6
- data/lib/topographer/importer/strategy/base.rb +43 -38
- data/lib/topographer/importer/strategy/create_or_update_record.rb +39 -34
- data/lib/topographer/importer/strategy/import_new_record.rb +16 -10
- data/lib/topographer/importer/strategy/import_status.rb +27 -20
- data/lib/topographer/importer/strategy/update_record.rb +28 -24
- data/lib/topographer/version.rb +1 -1
- data/spec/assets/test_files/a_csv.csv +3 -0
- data/spec/topographer/importer/helpers/write_log_to_csv_spec.rb +4 -4
- data/spec/topographer/importer/importer_spec.rb +21 -5
- data/spec/topographer/importer/input/delimited_spreadsheet_spec.rb +90 -0
- data/spec/topographer/importer/input/source_data_spec.rb +2 -2
- data/spec/topographer/importer/logger/base_spec.rb +19 -0
- data/spec/topographer/importer/logger/fatal_error_entry_spec.rb +2 -2
- data/spec/topographer/importer/logger/simple_spec.rb +3 -3
- data/spec/topographer/importer/mapper/default_field_mapping_spec.rb +2 -2
- data/spec/topographer/importer/mapper/field_mapping_spec.rb +21 -21
- data/spec/topographer/importer/mapper/mapper_builder_spec.rb +9 -9
- data/spec/topographer/importer/mapper/mapping_validator_spec.rb +10 -10
- data/spec/topographer/importer/mapper/validation_field_mapping_spec.rb +3 -3
- data/spec/topographer/importer/mapper_spec.rb +25 -25
- data/spec/topographer/importer/strategy/base_spec.rb +16 -5
- data/spec/topographer/importer/strategy/create_or_update_record_spec.rb +3 -3
- data/spec/topographer/importer/strategy/import_new_records_spec.rb +5 -5
- data/spec/topographer/importer/strategy/mapped_model.rb +9 -0
- data/spec/topographer/importer/strategy/update_record_spec.rb +3 -3
- data/topographer.gemspec +3 -2
- metadata +101 -102
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f03c6ff70ebbd6f7dcb8c6b4d72deb2de304aee0
|
4
|
+
data.tar.gz: 22c2fd768c6c4d17586b46f0755d4ee56590fe6c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 0c51ad3208acba7412522563cb4d1693c4de97fe30f9e392ca07bddbc3832a050bbb2dd96a8c48313e5025beda6ad50c132c4f310053050d17985c010f884e6b
|
7
|
+
data.tar.gz: a307fa166260f5dc6e8e44b6099fe5842b4e477bb941922ed0f44f92b8e4fca2c619e2c3d24d861655bca0477fe99ae2fbc372381d85a3b8f13172c9fac062b6
|
@@ -1,4 +1,6 @@
|
|
1
|
-
|
2
|
-
class
|
3
|
-
class
|
1
|
+
module Topographer
|
2
|
+
class InvalidMappingError < StandardError; end
|
3
|
+
class InvalidStructureError < Topographer::InvalidMappingError; end
|
4
|
+
class MappingFailure < Topographer::InvalidMappingError; end
|
5
|
+
end
|
4
6
|
|
data/lib/topographer/importer.rb
CHANGED
@@ -17,17 +17,21 @@ class Topographer::Importer
|
|
17
17
|
importer.logger
|
18
18
|
end
|
19
19
|
|
20
|
-
|
20
|
+
# @param mapping_generator [#get_mapper] the object responsible for deciding which mapping to use for the strategy
|
21
|
+
# @param strategy either a Class that inherits from Topographer::Importer::Strategy::Base or an instance of a strategy
|
22
|
+
def initialize(input, mapping_generator, strategy, logger, options = {})
|
21
23
|
@logger = logger
|
22
24
|
@fatal_errors = []
|
23
25
|
|
24
26
|
dry_run = options.fetch(:dry_run, false)
|
25
27
|
ignore_unmapped_columns = options.fetch(:ignore_unmapped_columns, false)
|
26
28
|
|
27
|
-
|
29
|
+
strategy_class = strategy.is_a?(Class) ? strategy : strategy.class
|
30
|
+
|
31
|
+
mapper = mapping_generator.get_mapper(strategy_class)
|
28
32
|
|
29
33
|
if importable?(input, mapper, ignore_unmapped_columns)
|
30
|
-
strategy =
|
34
|
+
strategy = setup_strategy(mapper, strategy, strategy_class)
|
31
35
|
strategy.dry_run = dry_run
|
32
36
|
import_data(strategy, input, mapper.model_class.name)
|
33
37
|
else
|
@@ -35,6 +39,14 @@ class Topographer::Importer
|
|
35
39
|
end
|
36
40
|
end
|
37
41
|
|
42
|
+
def setup_strategy(mapper, strategy, strategy_class)
|
43
|
+
if strategy == strategy_class
|
44
|
+
strategy_class.new(mapper) # supports legacy code
|
45
|
+
else
|
46
|
+
strategy.mapper = mapper
|
47
|
+
strategy
|
48
|
+
end
|
49
|
+
end
|
38
50
|
|
39
51
|
def import_data(strategy, input, import_class)
|
40
52
|
input.each do |data|
|
@@ -52,11 +64,15 @@ class Topographer::Importer
|
|
52
64
|
end
|
53
65
|
end
|
54
66
|
|
55
|
-
def invalid_header_message(mapper)
|
56
|
-
'Invalid Input Header -
|
57
|
-
|
58
|
-
|
59
|
-
|
67
|
+
def invalid_header_message(mapper, ignore_unmapped_columns = false)
|
68
|
+
error = 'Invalid Input Header -'
|
69
|
+
if mapper.missing_columns.any?
|
70
|
+
error << " Missing Columns: #{mapper.missing_columns.join(', ')}"
|
71
|
+
end
|
72
|
+
if mapper.bad_columns.any? && !ignore_unmapped_columns
|
73
|
+
error << " Invalid Columns: #{mapper.bad_columns.join(', ')}"
|
74
|
+
end
|
75
|
+
error
|
60
76
|
end
|
61
77
|
|
62
78
|
def importable?(input, mapper, ignore_unmapped_columns)
|
@@ -72,7 +88,7 @@ class Topographer::Importer
|
|
72
88
|
def valid_header?(input, mapper, ignore_unmapped_columns)
|
73
89
|
valid = mapper.input_structure_valid?(input.get_header, ignore_unmapped_columns: ignore_unmapped_columns)
|
74
90
|
|
75
|
-
fatal_errors << invalid_header_message(mapper) unless valid
|
91
|
+
fatal_errors << invalid_header_message(mapper, ignore_unmapped_columns) unless valid
|
76
92
|
|
77
93
|
valid
|
78
94
|
end
|
@@ -1,18 +1,24 @@
|
|
1
|
-
|
2
|
-
require_relative 'helpers/write_log_to_csv'
|
1
|
+
require_relative 'helpers/write_log_to_csv'
|
3
2
|
|
4
|
-
|
5
|
-
|
3
|
+
module Topographer
|
4
|
+
class Importer
|
5
|
+
module Helpers
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
7
|
+
def boolify(word)
|
8
|
+
return nil if word.nil?
|
9
|
+
|
10
|
+
case word.downcase
|
11
|
+
when 'yes'
|
12
|
+
true
|
13
|
+
when 'no'
|
14
|
+
false
|
15
|
+
when 'true'
|
16
|
+
true
|
17
|
+
when 'false'
|
18
|
+
false
|
19
|
+
end
|
20
|
+
end
|
16
21
|
end
|
17
22
|
end
|
18
23
|
end
|
24
|
+
|
@@ -1,85 +1,93 @@
|
|
1
1
|
require 'singleton'
|
2
2
|
require 'csv'
|
3
|
-
class Topographer::Importer::Helpers::WriteLogToCSV
|
4
|
-
include Singleton
|
5
3
|
|
6
|
-
|
4
|
+
module Topographer
|
5
|
+
class Importer
|
6
|
+
module Helpers
|
7
|
+
class WriteLogToCSV
|
8
|
+
include Singleton
|
7
9
|
|
8
|
-
|
10
|
+
def initialize
|
11
|
+
|
12
|
+
end
|
9
13
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
14
|
+
def write_log_to_csv(log, output_file_path, options = {})
|
15
|
+
@log = log
|
16
|
+
@write_all = options.fetch(:write_all, true)
|
17
|
+
CSV.open(output_file_path, 'wb') do |csv_file|
|
18
|
+
csv_file << get_detail_header
|
19
|
+
csv_file << get_details
|
20
|
+
csv_file << get_log_header if @log.entries?
|
21
|
+
@log.all_entries.each do |entry|
|
22
|
+
if entry.failure? || @write_all
|
19
23
|
|
20
|
-
|
24
|
+
csv_file << format_log_entry(entry)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
21
28
|
end
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
25
29
|
|
26
|
-
|
30
|
+
private
|
27
31
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
32
|
+
def get_detail_header
|
33
|
+
if @write_all
|
34
|
+
['Fatal Errors', 'Total Imports', 'Successful Imports', 'Failed Imports']
|
35
|
+
else
|
36
|
+
['Fatal Errors', 'Failed Imports']
|
37
|
+
end
|
38
|
+
end
|
35
39
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
40
|
+
def get_details
|
41
|
+
details = []
|
42
|
+
details << ((@log.fatal_error?) ? @log.fatal_errors.size : 'None')
|
43
|
+
if @write_all
|
44
|
+
details << @log.total_imports
|
45
|
+
details << @log.successful_imports
|
46
|
+
end
|
47
|
+
details << @log.failed_imports
|
48
|
+
end
|
45
49
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
50
|
+
def get_log_header
|
51
|
+
header = []
|
52
|
+
header << 'Input Identifier'
|
53
|
+
header << 'Source Identifier'
|
54
|
+
header << 'Model Class'
|
55
|
+
header << 'Timestamp'
|
56
|
+
header << 'Status'
|
57
|
+
header << 'Message'
|
58
|
+
header << 'Details'
|
59
|
+
end
|
56
60
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
61
|
+
def format_log_entry(log_entry)
|
62
|
+
entry = []
|
63
|
+
entry << log_entry.input_identifier
|
64
|
+
entry << log_entry.source_identifier
|
65
|
+
entry << log_entry.model_name
|
66
|
+
entry << log_entry.timestamp.strftime('%F - %T:%L')
|
67
|
+
entry << ((log_entry.success?) ? 'Success' : 'Failure')
|
68
|
+
entry << log_entry.message
|
69
|
+
entry << format_log_entry_details(log_entry.details)
|
70
|
+
end
|
67
71
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
72
|
+
def format_log_entry_details(details)
|
73
|
+
if details
|
74
|
+
formatted_details = []
|
75
|
+
details.keys.each do |key|
|
76
|
+
detail_string = ''
|
77
|
+
detail_messages = Array(details[key])
|
78
|
+
if detail_messages.any?
|
79
|
+
detail_string << key.to_s.capitalize << ': '
|
80
|
+
detail_string << detail_messages.join(', ')
|
81
|
+
end
|
82
|
+
formatted_details << detail_string unless detail_string.empty?
|
83
|
+
end
|
84
|
+
formatted_details.join("; ")
|
85
|
+
else
|
86
|
+
''
|
77
87
|
end
|
78
|
-
formatted_details << detail_string unless detail_string.empty?
|
79
88
|
end
|
80
|
-
formatted_details.join("; ")
|
81
|
-
else
|
82
|
-
''
|
83
89
|
end
|
84
90
|
end
|
91
|
+
end
|
85
92
|
end
|
93
|
+
|
@@ -1,5 +1,12 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
1
|
+
require_relative 'input/source_data'
|
2
|
+
require_relative 'input/base'
|
3
|
+
require_relative 'input/roo'
|
4
|
+
require_relative 'input/delimited_spreadsheet'
|
5
|
+
|
6
|
+
module Topographer
|
7
|
+
class Importer
|
8
|
+
module Input
|
9
|
+
|
10
|
+
end
|
11
|
+
end
|
5
12
|
end
|
@@ -1,21 +1,27 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
1
|
+
module Topographer
|
2
|
+
class Importer
|
3
|
+
module Input
|
4
|
+
class Base
|
5
|
+
def get_header
|
6
|
+
raise NotImplementedError
|
7
|
+
end
|
5
8
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
+
def input_identifier
|
10
|
+
raise NotImplementedError
|
11
|
+
end
|
9
12
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
+
def each
|
14
|
+
raise NotImplementedError
|
15
|
+
end
|
13
16
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
+
def importable?
|
18
|
+
true
|
19
|
+
end
|
17
20
|
|
18
|
-
|
19
|
-
|
21
|
+
def failure_message
|
22
|
+
''
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
20
26
|
end
|
21
27
|
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Topographer
|
2
|
+
class Importer
|
3
|
+
module Input
|
4
|
+
class DelimitedSpreadsheet < Topographer::Importer::Input::Base
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
# Creates a new DelimitedSpreadsheet input wrapper. NOTE: Since Topographer relies on headers
|
8
|
+
# to map from input to output columns, you should enable header parsing in the CSV object passed in
|
9
|
+
#
|
10
|
+
# NOTE: the CSV used to construct this object should have the :return_headers flag set or the first
|
11
|
+
# row of data will be lost!
|
12
|
+
#
|
13
|
+
# @param name [String] the name of the delimited file being dealt with (e.g. My Data File 1)
|
14
|
+
# @param spreadsheet [CSV] the spreadsheet object to be parsed
|
15
|
+
def initialize(name, spreadsheet)
|
16
|
+
@sheet = spreadsheet
|
17
|
+
@name = name
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns the headers in the CSV file, or if header parsing is not enabled, an empty array
|
21
|
+
#
|
22
|
+
# @return [Array<String>] the headers in the file
|
23
|
+
def get_header
|
24
|
+
unless @header
|
25
|
+
if @sheet.headers === true
|
26
|
+
@sheet.shift
|
27
|
+
elsif @sheet.headers.nil?
|
28
|
+
@header = []
|
29
|
+
end
|
30
|
+
@header ||= @sheet.headers
|
31
|
+
end
|
32
|
+
|
33
|
+
@header
|
34
|
+
end
|
35
|
+
|
36
|
+
def input_identifier
|
37
|
+
@name
|
38
|
+
end
|
39
|
+
|
40
|
+
def each
|
41
|
+
@sheet.each_with_index do |data, index|
|
42
|
+
row_number = index + 2
|
43
|
+
source_identifier = "Row: #{row_number}"
|
44
|
+
|
45
|
+
|
46
|
+
yield Topographer::Importer::Input::SourceData.new(
|
47
|
+
source_identifier,
|
48
|
+
data.to_h
|
49
|
+
)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -1,33 +1,40 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
module Topographer
|
2
|
+
class Importer
|
3
|
+
module Input
|
4
|
+
class Roo < Topographer::Importer::Input::Base
|
5
|
+
include Enumerable
|
3
6
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
7
|
+
def initialize(roo_sheet, header_row=1, data_row=2)
|
8
|
+
@sheet = roo_sheet
|
9
|
+
@header = @sheet.row(header_row).map(&:strip)
|
10
|
+
@start_data_row = data_row
|
11
|
+
@end_data_row = @sheet.last_row
|
12
|
+
end
|
10
13
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
+
def get_header
|
15
|
+
@header
|
16
|
+
end
|
14
17
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
18
|
+
def input_identifier
|
19
|
+
#This is apparently how you get the name of the sheet...this makes me sad
|
20
|
+
@sheet.default_sheet
|
21
|
+
end
|
19
22
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
23
|
+
def each
|
24
|
+
@start_data_row.upto @end_data_row do |row_number|
|
25
|
+
data = @sheet.row(row_number)
|
26
|
+
source_identifier = "Row: #{row_number}"
|
24
27
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
28
|
+
if data.reject{ |column| column.blank? }.any?
|
29
|
+
yield Topographer::Importer::Input::SourceData.new(
|
30
|
+
source_identifier,
|
31
|
+
Hash[@header.zip(data)]
|
32
|
+
)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
30
36
|
end
|
37
|
+
|
31
38
|
end
|
32
39
|
end
|
33
40
|
end
|