rails_csv_importer 0.1.3

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 ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2012 Ben Li
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.
21
+
data/README.rdoc ADDED
@@ -0,0 +1,123 @@
1
+ = RailsCsvImporter
2
+
3
+ Define configuration in a hash and then import Ruby on Rails model data from CSV with one method call.
4
+
5
+ Both Rails 2 and 3 are supported.
6
+
7
+ == Usage
8
+
9
+ 1. Add acts_as_rails_csv_importer to your model
10
+ 2. Call YourModel.get_csv_import_template to generate the CSV template.
11
+ 3. Call YourModel.import_from_csv in your controller to import data from a CSV file uploaded.
12
+
13
+ === In a Rails Model
14
+
15
+ class Foo < ActiveRecord::Base
16
+ acts_as_rails_csv_importer
17
+ end
18
+
19
+ === Generate CSV Template
20
+
21
+ Call Foo.get_csv_import_template(import_config). See below for details on the parameter.
22
+
23
+ === Import From CSV
24
+
25
+ Call Foo.import_from_csv(import_config, content, options = {})
26
+
27
+ * import_config
28
+
29
+ A hash that defines what data are imported and how.
30
+
31
+ [:mapping] A hash that defines the columns in the csv. Each hash key is the model attribute name for the column.
32
+ Each value is a hash defining how the column is processed:
33
+
34
+ [:name] The heading that identify the column in the csv.
35
+ If omitted, the heading will be the humanized form of the key.
36
+ (_Optional_)
37
+
38
+ There are four different ways to specify how the column is processed:
39
+
40
+ [:value_method] A lambda to convert the column value from csv into the attribute value in the model.
41
+ Class Acts::RailsCsvImporter::ValueMethods provides some pre-defined value methods.
42
+ (_Optional_)
43
+
44
+ [:record_method] A lambda to find the associated record if this attribue is a foreign key.
45
+ (_Optional_)
46
+
47
+ [:virtual] If set to true, the column is not mapped to a model attribute.
48
+ Usually the column will be used together with other columns.
49
+
50
+ [None of the above options present] The column value will be used without any conversion.
51
+
52
+ [:find_existing] A lambda to find the existing record to update for a row.
53
+ A new record is created if the lambda could not find an existing record and return nil.
54
+ The row is passed as a hash to the lambda.
55
+ If this parameter is omitted, a new record is created for each row.
56
+ (_Optional_)
57
+
58
+ * content
59
+
60
+ A string that contains the csv content to import from.
61
+
62
+ * options
63
+
64
+ A hash of options for the import. (_Optional_)
65
+
66
+ [:partial_save] If true, any valid rows in the csv will be saved even if there are some invalid rows.
67
+ Otherwise, data will be imported only when all rows are valid.
68
+
69
+ Exception +Acts::RailsCsvImporter::RailsCsvImportError+ is thrown when there are errors duing the importing.
70
+
71
+ == Example
72
+
73
+ class Material < ActiveRecord::Base
74
+ acts_as_rails_csv_importer
75
+ end
76
+
77
+ class MaterialsController < ApplicationController
78
+ IMPORT_CONFIG = {
79
+ :mapping => {
80
+ 'name' => {},
81
+ 'fragile' => {
82
+ :name => "Fragile?",
83
+ :value_method => Acts::RailsCsvImporter::ValueMethods.boolean_value_method
84
+ },
85
+ 'category_id' => {:record_method => lambda { |v, row, mapping| Category.find_by_name(v) } },
86
+ },
87
+ :find_existing => lambda { |row| Material.find_by_name(row['name']) }
88
+ }
89
+
90
+ def download_template
91
+ headers["Content-Type"] = 'text/csv'
92
+ headers["Content-Disposition"] = 'attachment; filename="template.csv"'
93
+ render :text => Material.get_import_template(IMPORT_CONFIG)
94
+ end
95
+
96
+ def import
97
+ num_imported = Material.import_from_csv(IMPORT_CONFIG, params[:file_upload])
98
+ flash[:notice] = num_imported.to_s + " records imported successfully."
99
+ rescue Acts::RailsCsvImporter::RailsCsvImportError => ex
100
+ @err_message = ex.errors.map { |err, row| err.is_a?(String) ? err : err.full_messages }.join(';')
101
+ render :template => :error
102
+ end
103
+ end
104
+
105
+ For more examples, refer to +test/rails_csv_importer_test.rb+
106
+
107
+ == Download and installation
108
+
109
+ The latest version of RailsCsvImporter can be installed with RubyGems:
110
+
111
+ % [sudo] gem install rails_csv_importer
112
+
113
+ Source code can be downloaded on GitHub
114
+
115
+ * https://github.com/benli/rails_csv_importer/tree/master
116
+
117
+
118
+ == License
119
+
120
+ RailsCsvImporter is released under the MIT license:
121
+
122
+ * http://www.opensource.org/licenses/MIT
123
+
@@ -0,0 +1,229 @@
1
+ if RUBY_VERSION >= "1.9"
2
+ # CSV in ruby 1.9 is FasterCSV plus support for Ruby 1.9's m17n encoding engine
3
+ require 'csv'
4
+ FasterCSV = CSV
5
+ else
6
+ require 'faster_csv'
7
+ end
8
+ require 'iconv'
9
+
10
+ module Acts # :nodoc
11
+ module RailsCsvImporter
12
+ #
13
+ # This class is the placeholder of methods common used.
14
+ #
15
+ class ValueMethods
16
+ #
17
+ # A value method that can be used on boolean type columns.
18
+ # Accept 'yes' and 'no' instead of the default 'true" and 'false'
19
+ #
20
+ def self.yes_no_value_method
21
+ lambda { |v, row, mapping|
22
+ case v.downcase
23
+ when 'yes' then true
24
+ when 'no' then false
25
+ else
26
+ raise "Value must be Yes or No"
27
+ end
28
+ }
29
+ end
30
+ end
31
+
32
+ #
33
+ # The exception thrown when there are errors in the import.
34
+ #
35
+ class RailsCsvImportError < RuntimeError
36
+ #
37
+ # An array of errors occurred during the import.
38
+ #
39
+ # Each error is an array with two elements:
40
+ # 1. Either a string of error message or the ActiveRecord::Errors object.
41
+ # 2. An array of columns in the row that is associated with this error.
42
+ #
43
+ attr_reader :errors
44
+
45
+ # An array of column headings.
46
+ attr_reader :header_row
47
+
48
+ # Number of rows already imported.
49
+ attr_reader :num_imported
50
+
51
+ def initialize(errors, header_row, num_imported) # :nodoc:
52
+ @errors = errors
53
+ @header_row = header_row
54
+ @num_imported = num_imported
55
+ end
56
+ end
57
+
58
+ def self.included(base) # :nodoc:
59
+ base.extend(ClassMethods)
60
+ end
61
+
62
+ module ClassMethods
63
+ #
64
+ # Called in a Rails model to bring it CSV Import funcationality provided in this gem.
65
+ #
66
+ def acts_as_rails_csv_importer
67
+ class_eval do
68
+ extend Acts::RailsCsvImporter::SingletonMethods
69
+ end
70
+ end
71
+ end
72
+
73
+ module SingletonMethods
74
+ #
75
+ # Import model data from csv content
76
+ #
77
+ # Options:
78
+ #
79
+ # * +import_config+ - specifies how the csv content is parsed.
80
+ # * +content+ - the csv content in a string that can be parsed by FasterCSV
81
+ # * +options+ - none for now
82
+ #
83
+ # Throws: +RailsCsvImportError+
84
+ #
85
+ # See +README.rdoc+ for details.
86
+ #
87
+ def import_from_csv(import_config, content, options = {})
88
+ ic = Iconv.new('UTF-8', 'UTF-8')
89
+
90
+ num_rows_saved=0
91
+ errors = []
92
+ header_row = []
93
+
94
+ mapping = import_config[:mapping]
95
+ # a hash for finding the key for a column heading quickly
96
+ name_to_column_hash = mapping.keys.inject({}) { |acc, key|
97
+ acc[translate_column(key, mapping).downcase] = key
98
+ acc
99
+ }
100
+
101
+ self.transaction do
102
+ all_rows = []
103
+ # the column keys in the order of the column headings
104
+ col_names = []
105
+ row_num = 0
106
+ col_num = 0
107
+
108
+ # first phase: parse the csv and store the result in all_rows
109
+
110
+ first_row = true
111
+ begin
112
+ FasterCSV.parse(content, :skip_blanks => true) do |row|
113
+ row_num += 1
114
+ col_num = 0
115
+ row = row.map { |col| col_num += 1; ic.iconv(col) }
116
+ if first_row == true
117
+ header_row = row
118
+ row.each { |column_name| col_names << name_to_column_hash[column_name.downcase] }
119
+ first_row = false
120
+ else
121
+ all_rows << row
122
+ row_hash = {}
123
+ row.each_with_index { |column, x| row_hash[col_names[x]] = column if col_names[x] }
124
+ end
125
+ end
126
+ rescue Iconv::IllegalSequence => ex
127
+ all_rows = header_row = []
128
+ errors << ["Invalid character encountered in row #{row_num}, column #{col_num} in the CSV file: #{ex.message}", []]
129
+ rescue FasterCSV::MalformedCSVError => ex
130
+ all_rows = header_row = []
131
+ errors << ["Invalid CSV format: #{ex.message}", []]
132
+ end
133
+
134
+ # second phase: process the rows
135
+
136
+ all_rows.each do |row|
137
+ row_hash = {}
138
+ row.each_with_index { |column, x| row_hash[col_names[x]] = column if col_names[x] }
139
+
140
+ find_existing = import_config[:find_existing]
141
+ if find_existing
142
+ record = find_existing.call(row_hash) || self.new
143
+ else
144
+ record = self.new
145
+ end
146
+
147
+ begin
148
+ row.each_with_index do |column, x|
149
+ col_name = col_names[x]
150
+ if col_name
151
+ col_config = mapping[col_name]
152
+ unless column.blank?
153
+ begin
154
+ # assign the correct value to the attribute according to the config
155
+ record[col_name] = if col_config[:value_method]
156
+ col_config[:value_method].call(column, row_hash, mapping)
157
+ elsif col_config[:record_method]
158
+ r = col_config[:record_method].call(column, row_hash, mapping)
159
+ raise "Unable to find referred record of value #{column}" if r.nil?
160
+ r.id
161
+ else
162
+ column
163
+ end
164
+ rescue Exception => e
165
+ raise "Failed to import column '#{header_row[x]}': #{e.message}"
166
+ end
167
+ end
168
+ end
169
+ end
170
+ rescue Exception => e
171
+ errors << [e.message, row]
172
+ next
173
+ end
174
+ if record.save
175
+ num_rows_saved += 1
176
+ else
177
+ errors << [record.errors, row]
178
+ end
179
+ end # all_rows
180
+
181
+ raise RailsCsvImportError.new(errors, header_row, num_rows_saved) if errors.any? && !options[:partial_save]
182
+ end # transaction
183
+
184
+ raise RailsCsvImportError.new(errors, header_row, num_rows_saved) if errors.any? && options[:partial_save]
185
+
186
+ num_rows_saved
187
+ end
188
+
189
+ #
190
+ # Return the csv import template in a string.
191
+ #
192
+ # options:
193
+ #
194
+ # * +import_config+ - specifies how the csv content is parsed when importing from the template.
195
+ # See +README.rdoc+ for details
196
+ #
197
+ def get_csv_import_template(import_config)
198
+ mapping = import_config[:mapping]
199
+ FasterCSV.generate do |csv|
200
+ csv << mapping.keys.map { |key| translate_column(key, mapping) }
201
+ end
202
+ end
203
+
204
+ private
205
+
206
+ #
207
+ # Translate a column key into its heading
208
+ #
209
+ # options:
210
+ #
211
+ # * +col+ - the key of the column
212
+ # * +mapping+ - the mapping parameter in the config
213
+ #
214
+ def translate_column(col, mapping)
215
+ if mapping[col][:name]
216
+ mapping[col][:name]
217
+ else
218
+ if col[-3,3] == "_id"
219
+ col = col[0, col.length - 3]
220
+ end
221
+ col.humanize
222
+ end
223
+ end
224
+ end
225
+ end
226
+ end
227
+
228
+ ActiveRecord::Base.send(:include, Acts::RailsCsvImporter) if defined?(ActiveRecord)
229
+
data/rails/init.rb ADDED
@@ -0,0 +1,2 @@
1
+ require 'rails_csv_importer'
2
+
metadata ADDED
@@ -0,0 +1,68 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rails_csv_importer
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 3
10
+ version: 0.1.3
11
+ platform: ruby
12
+ authors:
13
+ - Ben Li
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-06-25 00:00:00 Z
19
+ dependencies: []
20
+
21
+ description: Define configuration in a hash and then import Ruby on Rails model data from CSV with one method call.
22
+ email: libin1231@gmail.com
23
+ executables: []
24
+
25
+ extensions: []
26
+
27
+ extra_rdoc_files: []
28
+
29
+ files:
30
+ - MIT-LICENSE
31
+ - README.rdoc
32
+ - lib/rails_csv_importer.rb
33
+ - rails/init.rb
34
+ homepage: http://rubygems.org/gems/rails_csv_importer
35
+ licenses: []
36
+
37
+ post_install_message:
38
+ rdoc_options: []
39
+
40
+ require_paths:
41
+ - lib
42
+ required_ruby_version: !ruby/object:Gem::Requirement
43
+ none: false
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ hash: 3
48
+ segments:
49
+ - 0
50
+ version: "0"
51
+ required_rubygems_version: !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ hash: 3
57
+ segments:
58
+ - 0
59
+ version: "0"
60
+ requirements: []
61
+
62
+ rubyforge_project:
63
+ rubygems_version: 1.8.24
64
+ signing_key:
65
+ specification_version: 3
66
+ summary: A little gem to ease data importing in Ruby on Rails
67
+ test_files: []
68
+