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.
Files changed (44) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.rdoc +56 -0
  3. data/Rakefile +37 -0
  4. data/lib/active_import.rb +13 -0
  5. data/lib/active_import/import_csv.rb +99 -0
  6. data/lib/active_import/import_excel.rb +110 -0
  7. data/lib/active_import/model_converter.rb +119 -0
  8. data/lib/active_import/version.rb +3 -0
  9. data/lib/generators/active_import/model_converter_generator.rb +25 -0
  10. data/lib/generators/active_import/templates/data.csv.erb +1 -0
  11. data/lib/generators/active_import/templates/model_converter.rb.erb +45 -0
  12. data/lib/tasks/active_import_tasks.rake +139 -0
  13. data/test/active_import_test.rb +7 -0
  14. data/test/dummy/Rakefile +7 -0
  15. data/test/dummy/app/assets/javascripts/application.js +9 -0
  16. data/test/dummy/app/assets/stylesheets/application.css +7 -0
  17. data/test/dummy/app/controllers/application_controller.rb +3 -0
  18. data/test/dummy/app/helpers/application_helper.rb +2 -0
  19. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  20. data/test/dummy/config.ru +4 -0
  21. data/test/dummy/config/application.rb +45 -0
  22. data/test/dummy/config/boot.rb +10 -0
  23. data/test/dummy/config/database.yml +25 -0
  24. data/test/dummy/config/environment.rb +5 -0
  25. data/test/dummy/config/environments/development.rb +30 -0
  26. data/test/dummy/config/environments/production.rb +60 -0
  27. data/test/dummy/config/environments/test.rb +39 -0
  28. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  29. data/test/dummy/config/initializers/inflections.rb +10 -0
  30. data/test/dummy/config/initializers/mime_types.rb +5 -0
  31. data/test/dummy/config/initializers/secret_token.rb +7 -0
  32. data/test/dummy/config/initializers/session_store.rb +8 -0
  33. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  34. data/test/dummy/config/locales/en.yml +5 -0
  35. data/test/dummy/config/routes.rb +58 -0
  36. data/test/dummy/db/test.sqlite3 +0 -0
  37. data/test/dummy/log/test.log +0 -0
  38. data/test/dummy/public/404.html +26 -0
  39. data/test/dummy/public/422.html +26 -0
  40. data/test/dummy/public/500.html +26 -0
  41. data/test/dummy/public/favicon.ico +0 -0
  42. data/test/dummy/script/rails +6 -0
  43. data/test/test_helper.rb +10 -0
  44. 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