active_import 0.0.2
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.
- data/MIT-LICENSE +20 -0
- data/README.rdoc +56 -0
- data/Rakefile +37 -0
- data/lib/active_import.rb +13 -0
- data/lib/active_import/import_csv.rb +99 -0
- data/lib/active_import/import_excel.rb +110 -0
- data/lib/active_import/model_converter.rb +119 -0
- data/lib/active_import/version.rb +3 -0
- data/lib/generators/active_import/model_converter_generator.rb +25 -0
- data/lib/generators/active_import/templates/data.csv.erb +1 -0
- data/lib/generators/active_import/templates/model_converter.rb.erb +45 -0
- data/lib/tasks/active_import_tasks.rake +139 -0
- data/test/active_import_test.rb +7 -0
- data/test/dummy/Rakefile +7 -0
- data/test/dummy/app/assets/javascripts/application.js +9 -0
- data/test/dummy/app/assets/stylesheets/application.css +7 -0
- data/test/dummy/app/controllers/application_controller.rb +3 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/config/application.rb +45 -0
- data/test/dummy/config/boot.rb +10 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +30 -0
- data/test/dummy/config/environments/production.rb +60 -0
- data/test/dummy/config/environments/test.rb +39 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/inflections.rb +10 -0
- data/test/dummy/config/initializers/mime_types.rb +5 -0
- data/test/dummy/config/initializers/secret_token.rb +7 -0
- data/test/dummy/config/initializers/session_store.rb +8 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +5 -0
- data/test/dummy/config/routes.rb +58 -0
- data/test/dummy/db/test.sqlite3 +0 -0
- data/test/dummy/log/test.log +0 -0
- data/test/dummy/public/404.html +26 -0
- data/test/dummy/public/422.html +26 -0
- data/test/dummy/public/500.html +26 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/dummy/script/rails +6 -0
- data/test/test_helper.rb +10 -0
- metadata +169 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2011 YOURNAME
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
= ActiveImport
|
2
|
+
|
3
|
+
THIS DOCUMENTATION IS RATHER INCOMPLETE, I AM WORKING ON IT.
|
4
|
+
|
5
|
+
== Installation
|
6
|
+
|
7
|
+
Simple add the following to your Gemfile
|
8
|
+
|
9
|
+
gem 'active_import'
|
10
|
+
|
11
|
+
Then run:
|
12
|
+
|
13
|
+
bundle install
|
14
|
+
|
15
|
+
== Quick Example
|
16
|
+
|
17
|
+
rails g active_import:model_converter User
|
18
|
+
|
19
|
+
This will create an model converter in the directory app/model_converters. You can read through this import file to see how the import works.
|
20
|
+
|
21
|
+
This also creates a default data file in db/active_import. This will be the CSV used for this converter.
|
22
|
+
|
23
|
+
== Usage
|
24
|
+
|
25
|
+
There are three Rake tasks that allow you to use a converter on a file:
|
26
|
+
|
27
|
+
rake active_import:csv Load csv file into a model using a model converter
|
28
|
+
rake active_import:excel Load excel file into a model using a model converter
|
29
|
+
rake active_import:seed Seed a list of import files
|
30
|
+
|
31
|
+
=== Examples
|
32
|
+
|
33
|
+
rake active_import:csv user.csv CONVERTER=User CONVERTER_OPTIONS="give_admin_access=true"
|
34
|
+
|
35
|
+
In this case the file in db/active_import/user.csv would be run through the converter UserConverter. The options specified are available within the converter. In this case @options["give_admin_access"] will evaluate to true.
|
36
|
+
|
37
|
+
== Seeding
|
38
|
+
|
39
|
+
Seeding allows you to import several files through different model converters in a single command. It involves the creation of a .seed file. Each file goes on a single line and the options are separated by pipe symbols.
|
40
|
+
|
41
|
+
=== Example
|
42
|
+
|
43
|
+
user_info/user.xls | User | give_admin_access=false,send_email=true
|
44
|
+
user_info/roles.xls | Role
|
45
|
+
user_info/permissions.xls | UserPermission
|
46
|
+
|
47
|
+
Note this will get the data files from a subdirectory: db/active_import/user_info. Also the top conversion uses options but as they are optional, the following two do not.
|
48
|
+
|
49
|
+
== The Converter Class
|
50
|
+
|
51
|
+
=== Column Setup
|
52
|
+
|
53
|
+
==== Mandatory Columns
|
54
|
+
==== Column Types
|
55
|
+
|
56
|
+
=== Custom Type Conversions
|
data/Rakefile
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
begin
|
3
|
+
require 'bundler/setup'
|
4
|
+
rescue LoadError
|
5
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
6
|
+
end
|
7
|
+
begin
|
8
|
+
require 'rdoc/task'
|
9
|
+
rescue LoadError
|
10
|
+
require 'rdoc/rdoc'
|
11
|
+
require 'rake/rdoctask'
|
12
|
+
RDoc::Task = Rake::RDocTask
|
13
|
+
end
|
14
|
+
|
15
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
16
|
+
rdoc.rdoc_dir = 'rdoc'
|
17
|
+
rdoc.title = 'ActiveImport'
|
18
|
+
rdoc.options << '--line-numbers'
|
19
|
+
rdoc.rdoc_files.include('README.rdoc')
|
20
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
|
25
|
+
Bundler::GemHelper.install_tasks
|
26
|
+
|
27
|
+
require 'rake/testtask'
|
28
|
+
|
29
|
+
Rake::TestTask.new(:test) do |t|
|
30
|
+
t.libs << 'lib'
|
31
|
+
t.libs << 'test'
|
32
|
+
t.pattern = 'test/**/*_test.rb'
|
33
|
+
t.verbose = false
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
task :default => :test
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require "active_import/import_csv"
|
2
|
+
require "active_import/import_excel"
|
3
|
+
require 'active_import/model_converter'
|
4
|
+
|
5
|
+
module ActiveImport
|
6
|
+
class Railtie < ::Rails::Railtie
|
7
|
+
railtie_name :active_import
|
8
|
+
|
9
|
+
rake_tasks do
|
10
|
+
load "tasks/active_import_tasks.rake"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module ActiveImport
|
2
|
+
if RUBY_VERSION =~ /^1.9/
|
3
|
+
require 'csv'
|
4
|
+
else
|
5
|
+
require 'fastercsv'
|
6
|
+
end
|
7
|
+
|
8
|
+
class ImportCsv
|
9
|
+
attr_reader :data_file, :converter, :estimated_rows
|
10
|
+
|
11
|
+
def initialize(model_converter, data_file)
|
12
|
+
@converter = model_converter
|
13
|
+
@data_file = data_file
|
14
|
+
end
|
15
|
+
|
16
|
+
def all_headers_found(headers)
|
17
|
+
mappings = @converter.columns
|
18
|
+
|
19
|
+
@missing_headers_mandatory = []
|
20
|
+
@missing_headers_optional = []
|
21
|
+
found_at_least_one = false
|
22
|
+
|
23
|
+
mappings.each_pair do |column_name, mapping|
|
24
|
+
if headers[column_name].nil?
|
25
|
+
if mapping[:mandatory]
|
26
|
+
@missing_headers_mandatory << column_name
|
27
|
+
else
|
28
|
+
@missing_headers_optional << column_name
|
29
|
+
end
|
30
|
+
else
|
31
|
+
found_at_least_one = true
|
32
|
+
end
|
33
|
+
end
|
34
|
+
if found_at_least_one
|
35
|
+
@missing_headers_optional.each { |field| puts "Missing optional column #{field}".yellow }
|
36
|
+
@missing_headers_mandatory.each { |field| puts "Missing mandatory column #{field}".red }
|
37
|
+
end
|
38
|
+
return false unless @missing_headers_mandatory.empty?
|
39
|
+
true
|
40
|
+
end
|
41
|
+
|
42
|
+
def parse(&block)
|
43
|
+
# Get an estimate of the number of rows in the file
|
44
|
+
f = File.open(data_file)
|
45
|
+
@estimated_rows = f.readlines.size - 1
|
46
|
+
f.close
|
47
|
+
|
48
|
+
column_mappings = @converter.columns
|
49
|
+
|
50
|
+
headers = {}
|
51
|
+
header = true
|
52
|
+
data_count = 0
|
53
|
+
row_number = 0
|
54
|
+
csv_class = nil
|
55
|
+
if RUBY_VERSION =~ /^1.9/
|
56
|
+
csv_class = CSV
|
57
|
+
puts "Using built in Ruby 1.9 CSV parser".cyan
|
58
|
+
else
|
59
|
+
csv_class = FasterCSV
|
60
|
+
puts "Using FasterCSV parser".cyan
|
61
|
+
end
|
62
|
+
|
63
|
+
csv_class.foreach(@data_file) do |row|
|
64
|
+
row_number += 1
|
65
|
+
if (header)
|
66
|
+
column = 0
|
67
|
+
row.each do |column_value|
|
68
|
+
column += 1
|
69
|
+
column_mappings.each do |column_name, mapping|
|
70
|
+
match = mapping[:match] || mapping[:header]
|
71
|
+
if /#{match}/.match(column_value)
|
72
|
+
puts "Found header for #{column_name} at column #{column}".green
|
73
|
+
if (headers[column_name].nil?)
|
74
|
+
headers[column_name] = column
|
75
|
+
else
|
76
|
+
puts "Found duplicate header '#{column_name}' on columns #{column} and #{headers[column_name]}.".red
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
unless all_headers_found(headers)
|
82
|
+
puts "Missing headers".red
|
83
|
+
break
|
84
|
+
end
|
85
|
+
header = false
|
86
|
+
else
|
87
|
+
import_row = {}
|
88
|
+
headers.each_pair do |name, column|
|
89
|
+
value = row[column - 1].to_s
|
90
|
+
import_row[name] = value
|
91
|
+
end
|
92
|
+
data_count += 1
|
93
|
+
yield import_row, @converter, row_number, @estimated_rows
|
94
|
+
end
|
95
|
+
end
|
96
|
+
puts "Imported #{data_count} rows".cyan
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module ActiveImport
|
2
|
+
require "roo"
|
3
|
+
class ImportExcel
|
4
|
+
attr_reader :data_file, :converter, :estimated_rows
|
5
|
+
|
6
|
+
def initialize(model_converter, data_file)
|
7
|
+
@converter = model_converter
|
8
|
+
@data_file = data_file
|
9
|
+
end
|
10
|
+
|
11
|
+
def all_headers_found(headers)
|
12
|
+
mappings = @converter.columns
|
13
|
+
|
14
|
+
@missing_headers_mandatory = []
|
15
|
+
@missing_headers_optional = []
|
16
|
+
found_at_least_one = false
|
17
|
+
|
18
|
+
mappings.each_pair do |column_name, mapping|
|
19
|
+
if headers[column_name].nil?
|
20
|
+
if mapping[:mandatory]
|
21
|
+
@missing_headers_mandatory << column_name
|
22
|
+
else
|
23
|
+
@missing_headers_optional << column_name
|
24
|
+
end
|
25
|
+
else
|
26
|
+
found_at_least_one = true
|
27
|
+
end
|
28
|
+
end
|
29
|
+
if found_at_least_one
|
30
|
+
@missing_headers_optional.each { |field| puts "Missing optional column #{field.to_s.yellow}" }
|
31
|
+
@missing_headers_mandatory.each { |field| puts "Missing mandatory column #{field.to_s.red}" }
|
32
|
+
end
|
33
|
+
return false unless @missing_headers_mandatory.empty?
|
34
|
+
true
|
35
|
+
end
|
36
|
+
|
37
|
+
def find_excel_header_row(e)
|
38
|
+
column_mappings = @converter.columns
|
39
|
+
|
40
|
+
e.sheets.each do |sheet|
|
41
|
+
puts "Looking for the header row in sheet #{sheet}".cyan
|
42
|
+
e.default_sheet = sheet
|
43
|
+
e.first_row.upto(e.last_row) do |row|
|
44
|
+
headers = {}
|
45
|
+
(e.first_column..e.last_column).each do |column|
|
46
|
+
column_mappings.each do |column_name, mapping|
|
47
|
+
match = mapping[:match] || mapping[:header]
|
48
|
+
if /#{match}/.match(e.cell(row, column).to_s)
|
49
|
+
puts "Found header for #{column_name.to_s.green} at column #{column} at row #{row}"
|
50
|
+
if (headers[column_name].nil?)
|
51
|
+
headers[column_name] = column
|
52
|
+
else
|
53
|
+
puts "Found duplicate header '#{column_name.to_s.red}' on columns #{column} and #{headers[column_name]}."
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
if all_headers_found(headers)
|
60
|
+
puts "All headers found on row #{row}".green
|
61
|
+
return {:row => row, :sheet => sheet, :headers => headers}
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
return nil
|
66
|
+
end
|
67
|
+
|
68
|
+
def parse(&block)
|
69
|
+
column_mappings = @converter.columns
|
70
|
+
|
71
|
+
excelx = false
|
72
|
+
case File.extname(data_file).downcase
|
73
|
+
when ".xls"
|
74
|
+
e = Excel.new(data_file)
|
75
|
+
when ".xlsx"
|
76
|
+
excelx = true
|
77
|
+
e = Excelx.new(data_file)
|
78
|
+
end
|
79
|
+
|
80
|
+
result = find_excel_header_row(e)
|
81
|
+
|
82
|
+
if result.nil?
|
83
|
+
puts "Could not find header row.".red
|
84
|
+
return
|
85
|
+
end
|
86
|
+
|
87
|
+
e.default_sheet = result[:sheet]
|
88
|
+
header_row = result[:row]
|
89
|
+
headers = result[:headers]
|
90
|
+
|
91
|
+
# Loop through the data
|
92
|
+
puts "Reading data from row #{header_row + 1} to #{e.last_row}"
|
93
|
+
@estimated_rows = e.last_row - header_row;
|
94
|
+
row_number = 0
|
95
|
+
(header_row + 1).upto(e.last_row) do |row|
|
96
|
+
row_number += 1
|
97
|
+
import_row = {}
|
98
|
+
headers.each_pair do |name, column|
|
99
|
+
if excelx
|
100
|
+
value = e.cell(row, column).to_s
|
101
|
+
else
|
102
|
+
value = e.cell(row, column).to_s
|
103
|
+
end
|
104
|
+
import_row[name] = value
|
105
|
+
end
|
106
|
+
yield import_row, @converter, row_number, @estimated_rows
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
module ActiveImport
|
2
|
+
class ModelConverter
|
3
|
+
attr_accessor :options, :columns
|
4
|
+
# TODO: columns should be a reader only once old code has been fixed up
|
5
|
+
attr_reader :converted_values, :raw_values
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@columns = {}
|
9
|
+
setup
|
10
|
+
end
|
11
|
+
|
12
|
+
def setup
|
13
|
+
end
|
14
|
+
|
15
|
+
def add_column column_name, options
|
16
|
+
@columns[column_name] = options
|
17
|
+
end
|
18
|
+
|
19
|
+
def before
|
20
|
+
end
|
21
|
+
|
22
|
+
def after
|
23
|
+
end
|
24
|
+
|
25
|
+
def print_columns
|
26
|
+
@columns.each_pair do |name, column|
|
27
|
+
puts name
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def csv_headers()
|
32
|
+
selected_columns = @columns
|
33
|
+
|
34
|
+
[].tap do |o|
|
35
|
+
selected_columns.each_value do |column|
|
36
|
+
o << (column[:header] || column[:match])
|
37
|
+
end
|
38
|
+
end.to_csv.html_safe
|
39
|
+
end
|
40
|
+
|
41
|
+
def csv_values(values)
|
42
|
+
selected_columns = @columns
|
43
|
+
|
44
|
+
[].tap do |o|
|
45
|
+
selected_columns.each_key do |column|
|
46
|
+
o << values[column]
|
47
|
+
end
|
48
|
+
end.to_csv.html_safe
|
49
|
+
end
|
50
|
+
|
51
|
+
def process_values(values)
|
52
|
+
@raw_values = values
|
53
|
+
@converted_values = convert_attributes(values)
|
54
|
+
end
|
55
|
+
|
56
|
+
def remove_nil_from_converted_values
|
57
|
+
@converted_values.delete_if { |k, v| v.nil? }
|
58
|
+
end
|
59
|
+
|
60
|
+
def remove_blank_from_converted_values
|
61
|
+
@converted_values.delete_if { |k, v| v.to_s.blank? }
|
62
|
+
end
|
63
|
+
|
64
|
+
def convert_attributes(values)
|
65
|
+
cv = {}
|
66
|
+
@columns.each_pair do |name, column|
|
67
|
+
cv[name] = convert_attribute(name, values)
|
68
|
+
end
|
69
|
+
cv
|
70
|
+
end
|
71
|
+
|
72
|
+
def convert_string(value)
|
73
|
+
value = value.to_i if (value.to_i == value.to_f) if /^\s*[\d]+(\.0+){0,1}\s*$/.match(value.to_s)
|
74
|
+
return nil if value.to_s.blank? || value.to_s.nil?
|
75
|
+
value.to_s
|
76
|
+
end
|
77
|
+
|
78
|
+
def convert_clean_string(value)
|
79
|
+
value = value.to_i if (value.to_i == value.to_f) if /^\s*[\d]+(\.0+){0,1}\s*$/.match(value.to_s)
|
80
|
+
value = value.gsub(/[^A-Za-z0-9 \.,\?'""!@#\$%\^&\*\(\)-_=\+;:<>\/\\\|\}\{\[\]`~]/, '').strip if value.is_a?(String)
|
81
|
+
return nil if value.to_s.blank? || value.to_s.nil?
|
82
|
+
value.to_s
|
83
|
+
end
|
84
|
+
|
85
|
+
def convert_boolean value
|
86
|
+
/^y|t/.match(value.strip.downcase) ? true : false
|
87
|
+
end
|
88
|
+
|
89
|
+
def convert_date s
|
90
|
+
return nil if (s.nil? || s.blank?)
|
91
|
+
return Date.strptime(s, "%d/%m/%y") if /^[0-9]{1,2}\/[0-9]{1,2}\/[0-9]{2}$/.match(s)
|
92
|
+
begin
|
93
|
+
result = Date.parse(s)
|
94
|
+
rescue
|
95
|
+
puts "Could not parse date ".red + "'#{s}'"
|
96
|
+
end
|
97
|
+
|
98
|
+
return result
|
99
|
+
end
|
100
|
+
|
101
|
+
def report
|
102
|
+
""
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def convert_attribute(attribute, values)
|
108
|
+
return nil if values[attribute].nil?
|
109
|
+
conversion_function = "convert_#{columns[attribute][:type].to_s}"
|
110
|
+
value = values[attribute]
|
111
|
+
if self.respond_to? conversion_function
|
112
|
+
value = eval("#{conversion_function} value")
|
113
|
+
end
|
114
|
+
value
|
115
|
+
end
|
116
|
+
|
117
|
+
|
118
|
+
end
|
119
|
+
end
|