rails_admin_import_no_encoding 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,16 @@
1
+ module RailsAdminImport
2
+ module Formats
3
+ class DummyImporter
4
+ def initialize(*)
5
+ end
6
+
7
+ def valid?
8
+ false
9
+ end
10
+
11
+ def error
12
+ I18n.t("admin.import.invalid_format")
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,47 @@
1
+ module RailsAdminImport
2
+ module Formats
3
+ class FileImporter
4
+ def initialize(import_model, params)
5
+ if params.has_key?(:file)
6
+ @filename = params[:file].tempfile
7
+ end
8
+ @import_model = import_model
9
+ end
10
+
11
+ attr_reader :filename, :error, :import_model
12
+
13
+ def valid?
14
+ if filename.nil?
15
+ @error = I18n.t("admin.import.missing_file")
16
+ false
17
+ else
18
+ true
19
+ end
20
+ end
21
+
22
+ def each(&block)
23
+ return enum_for(:each) unless block_given?
24
+
25
+ if RailsAdminImport.config.logging && filename
26
+ copy_uploaded_file_to_log_dir
27
+ end
28
+
29
+ each_record(&block)
30
+ end
31
+
32
+ private
33
+
34
+ def each_record
35
+ raise "Implement each_record in subclasses"
36
+ end
37
+
38
+ def copy_uploaded_file_to_log_dir
39
+ copy_filename = "#{Time.now.strftime("%Y-%m-%d-%H-%M-%S")}-import.csv"
40
+ dir_path = File.join(Rails.root, "log", "import")
41
+ FileUtils.mkdir_p(dir_path)
42
+ copy_path = File.join(dir_path, copy_filename)
43
+ FileUtils.copy(filename, copy_path)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,32 @@
1
+ module RailsAdminImport
2
+ module Formats
3
+ class JSONImporter < FileImporter
4
+ Formats.register(:json, self)
5
+ Formats.register(:JSON, self)
6
+
7
+ # A method that yields a hash of attributes for each record to import
8
+ def each_record
9
+ File.open(filename) do |file|
10
+ data = JSON.load(file)
11
+
12
+ if data.is_a? Hash
13
+ # Load array from root key
14
+ data = data[root_key]
15
+ end
16
+
17
+ if !data.is_a? Array
18
+ raise ArgumentError, I18n.t("admin.import.invalid_json", root_key: root_key)
19
+ end
20
+
21
+ data.each do |record|
22
+ yield record.symbolize_keys
23
+ end
24
+ end
25
+ end
26
+
27
+ def root_key
28
+ import_model.model.model_name.element.pluralize
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,50 @@
1
+ require "csv"
2
+
3
+ module RailsAdminImport
4
+ module Formats
5
+ class XLSXImporter < FileImporter
6
+ Formats.register(:xlsx, self)
7
+ Formats.register(:XLSX, self)
8
+
9
+ autoload :SimpleXlsxReader, "simple_xlsx_reader"
10
+
11
+ def initialize(import_model, params)
12
+ super
13
+ @header_converter = RailsAdminImport.config.header_converter
14
+ end
15
+
16
+ # A method that yields a hash of attributes for each record to import
17
+ def each_record
18
+ doc = SimpleXlsxReader.open(filename)
19
+ sheet = doc.sheets.first
20
+ @headers = convert_headers(sheet.headers)
21
+ sheet.data.each do |row|
22
+ attr = convert_to_attributes(row)
23
+ yield attr unless attr.all? { |field, value| value.blank? }
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def convert_headers(headers)
30
+ headers.map do |h|
31
+ @header_converter.call(h || "")
32
+ end
33
+ end
34
+
35
+ def convert_to_attributes(row)
36
+ row_with_headers = @headers.zip(row)
37
+ row_with_headers.each_with_object({}) do |(field, value), record|
38
+ next if field.nil?
39
+ field = field.to_sym
40
+ if import_model.has_multiple_values?(field)
41
+ field = import_model.pluralize_field(field)
42
+ (record[field] ||= []) << value
43
+ else
44
+ record[field] = value
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,34 @@
1
+ module RailsAdminImport
2
+ module Formats
3
+ class << self
4
+ def register(format, klass)
5
+ @registry[format.to_s] = klass
6
+ end
7
+
8
+ def from_file(file)
9
+ return unless file
10
+ File.extname(file.original_filename).sub(/^\./, '')
11
+ end
12
+
13
+ def for(format, *args)
14
+ @registry.fetch(format.to_s, DummyImporter).new(*args)
15
+ end
16
+
17
+ def all
18
+ @registry.keys
19
+ end
20
+
21
+ def reset
22
+ @registry = {}
23
+ end
24
+ end
25
+
26
+ reset
27
+ end
28
+ end
29
+
30
+ require "rails_admin_import/formats/dummy_importer"
31
+ require "rails_admin_import/formats/file_importer"
32
+ require "rails_admin_import/formats/csv_importer"
33
+ require "rails_admin_import/formats/json_importer"
34
+ require "rails_admin_import/formats/xlsx_importer"
@@ -0,0 +1,17 @@
1
+ module RailsAdminImport
2
+ class ImportLogger
3
+ attr_reader :logger
4
+
5
+ def initialize(log_file_name = "rails_admin_import.log")
6
+ if RailsAdminImport.config.logging
7
+ @logger = Logger.new(File.join(Rails.root, "log", log_file_name))
8
+ end
9
+ end
10
+
11
+ def info(message)
12
+ if RailsAdminImport.config.logging
13
+ @logger.info message
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,112 @@
1
+ module RailsAdminImport
2
+ class AssociationNotFound < StandardError
3
+ end
4
+
5
+ class ImportModel
6
+ def initialize(abstract_model)
7
+ @abstract_model = abstract_model
8
+ @config = abstract_model.config.import
9
+ @model = abstract_model.model
10
+ end
11
+
12
+ attr_reader :abstract_model, :model, :config
13
+
14
+ def display_name
15
+ abstract_model.config.label
16
+ end
17
+
18
+ def label_for_model(object)
19
+ object.public_send(label_method)
20
+ end
21
+
22
+ def label_method
23
+ @label_method ||= abstract_model.config.object_label_method
24
+ end
25
+
26
+ def importable_fields(model_config = config)
27
+ @importable_fields ||= {}
28
+ @importable_fields[model_config] ||= model_config.visible_fields.reject do |f|
29
+ # Exclude id, created_at and updated_at
30
+ model_config.default_excluded_fields.include? f.name
31
+ end
32
+ end
33
+
34
+ def model_fields(model_config = config)
35
+ @model_fields ||= {}
36
+ @model_fields[model_config] ||= importable_fields(model_config).select { |f|
37
+ !f.association? || f.association.polymorphic?
38
+ }
39
+ end
40
+
41
+ def association_fields
42
+ @association_fields ||= importable_fields.select { |f|
43
+ f.association? && !f.association.polymorphic?
44
+ }
45
+ end
46
+
47
+ def single_association_fields
48
+ @single_association_fields ||= association_fields.select { |f|
49
+ !f.multiple?
50
+ }
51
+ end
52
+
53
+ def belongs_to_fields
54
+ @belongs_to_fields ||= single_association_fields.select { |f|
55
+ f.type == :belongs_to_association
56
+ }
57
+ end
58
+
59
+ def many_association_fields
60
+ @many_association_fields ||= association_fields.select { |f|
61
+ f.multiple?
62
+ }
63
+ end
64
+
65
+ def update_lookup_field_names
66
+ if @config.mapping_key_list.present?
67
+ @update_lookup_field_names = @config.mapping_key_list
68
+ else
69
+ @update_lookup_field_names ||= model_fields.map(&:name) + belongs_to_fields.map(&:foreign_key)
70
+ end
71
+ end
72
+
73
+ def associated_object(field, mapping_field, value)
74
+ klass = association_class(field)
75
+ klass.where(mapping_field => value).first or
76
+ raise AssociationNotFound, "#{klass}.#{mapping_field} = #{value}"
77
+ end
78
+
79
+ def association_class(field)
80
+ field.association.klass
81
+ end
82
+
83
+ def associated_config(field)
84
+ field.associated_model_config.import
85
+ end
86
+
87
+ def associated_model_fields(field)
88
+ @associated_fields ||= {}
89
+ if associated_config(field).mapping_key_list.present?
90
+ @associated_fields[field] ||= associated_config(field).mapping_key_list
91
+ else
92
+ @associated_fields[field] ||= associated_config(field).visible_fields.select { |f|
93
+ !f.association?
94
+ }.map(&:name)
95
+ end
96
+ end
97
+
98
+ def has_multiple_values?(field_name)
99
+ plural_name = pluralize_field(field_name)
100
+ many_association_fields.any? { |field| field.name == field_name || field.name == plural_name }
101
+ end
102
+
103
+ def pluralize_field(field_name)
104
+ @plural_fields ||= many_association_fields.map(&:name).each_with_object({}) { |name, h|
105
+ h[name.to_s.singularize.to_sym] = name
106
+ }
107
+ @plural_fields[field_name] || field_name
108
+ end
109
+ end
110
+ end
111
+
112
+
@@ -0,0 +1,248 @@
1
+ require "rails_admin_import/import_logger"
2
+
3
+ module RailsAdminImport
4
+ class Importer
5
+ def initialize(import_model, params)
6
+ @import_model = import_model
7
+ @params = params
8
+ end
9
+
10
+ attr_reader :import_model, :params
11
+
12
+ class UpdateLookupError < StandardError
13
+ end
14
+
15
+ def import(records)
16
+ begin
17
+ init_results
18
+
19
+ if records.count > RailsAdminImport.config.line_item_limit
20
+ return results = {
21
+ success: [],
22
+ error: [I18n.t('admin.import.import_error.line_item_limit', limit: RailsAdminImport.config.line_item_limit)]
23
+ }
24
+ end
25
+
26
+ perform_global_callback(:before_import)
27
+
28
+ with_transaction do
29
+ records.each do |record|
30
+ catch :skip do
31
+ import_record(record)
32
+ end
33
+ end
34
+
35
+ rollback_if_error
36
+ end
37
+
38
+ perform_global_callback(:after_import)
39
+ rescue Exception => e
40
+ report_general_error("#{e} (#{e.backtrace.first})")
41
+ end
42
+
43
+ format_results
44
+ end
45
+
46
+ private
47
+
48
+ def init_results
49
+ @results = { :success => [], :error => [] }
50
+ end
51
+
52
+ def with_transaction(&block)
53
+ if RailsAdminImport.config.rollback_on_error &&
54
+ defined?(ActiveRecord)
55
+
56
+ ActiveRecord::Base.transaction &block
57
+ else
58
+ block.call
59
+ end
60
+ end
61
+
62
+ def rollback_if_error
63
+ if RailsAdminImport.config.rollback_on_error &&
64
+ defined?(ActiveRecord) &&
65
+ !results[:error].empty?
66
+
67
+ results[:success] = []
68
+ raise ActiveRecord::Rollback
69
+ end
70
+ end
71
+
72
+ def import_record(record)
73
+ perform_model_callback(import_model.model, :before_import_find, record)
74
+
75
+ if update_lookup && !(update_lookup - record.keys).empty?
76
+ raise UpdateLookupError, I18n.t("admin.import.missing_update_lookup")
77
+ end
78
+
79
+ object = find_or_create_object(record, update_lookup)
80
+ return if object.nil?
81
+ action = object.new_record? ? :create : :update
82
+
83
+ begin
84
+ perform_model_callback(object, :before_import_associations, record)
85
+ import_single_association_data(object, record)
86
+ import_many_association_data(object, record)
87
+ rescue AssociationNotFound => e
88
+ error = I18n.t("admin.import.association_not_found", :error => e.to_s)
89
+ report_error(object, action, error)
90
+ perform_model_callback(object, :after_import_association_error, record)
91
+ return
92
+ end
93
+
94
+ perform_model_callback(object, :before_import_save, record)
95
+
96
+ if object.save
97
+ report_success(object, action)
98
+ perform_model_callback(object, :after_import_save, record)
99
+ else
100
+ report_error(object, action, object.errors.full_messages.join(", "))
101
+ perform_model_callback(object, :after_import_error, record)
102
+ end
103
+ end
104
+
105
+ def update_lookup
106
+ @update_lookup ||= if params[:update_if_exists] == "1"
107
+ params[:update_lookup].map(&:to_sym)
108
+ end
109
+ end
110
+
111
+ attr_reader :results
112
+
113
+ def logger
114
+ @logger ||= ImportLogger.new
115
+ end
116
+
117
+ def report_success(object, action)
118
+ object_label = import_model.label_for_model(object)
119
+ message = I18n.t("admin.import.import_success.#{action}",
120
+ :name => object_label)
121
+ logger.info "#{Time.now}: #{message}"
122
+ results[:success] << message
123
+ end
124
+
125
+ def report_error(object, action, error)
126
+ object_label = import_model.label_for_model(object)
127
+ message = I18n.t("admin.import.import_error.#{action}",
128
+ :name => object_label,
129
+ :error => error)
130
+ logger.info "#{Time.now}: #{message}"
131
+ results[:error] << message
132
+ end
133
+
134
+ def report_general_error(error)
135
+ message = I18n.t("admin.import.import_error.general", :error => error)
136
+ logger.info "#{Time.now}: #{message}"
137
+ results[:error] << message
138
+ end
139
+
140
+ def format_results
141
+ imported = results[:success]
142
+ not_imported = results[:error]
143
+ unless imported.empty?
144
+ results[:success_message] = format_result_message("successful", imported)
145
+ end
146
+ unless not_imported.empty?
147
+ results[:error_message] = format_result_message("error", not_imported)
148
+ end
149
+
150
+ results
151
+ end
152
+
153
+ def format_result_message(type, array)
154
+ result_count = "#{array.size} #{import_model.display_name.pluralize(array.size)}"
155
+ I18n.t("admin.flash.#{type}",
156
+ name: result_count,
157
+ action: I18n.t("admin.actions.import.done"))
158
+ end
159
+
160
+ def perform_model_callback(object, method_name, record)
161
+ if object.respond_to?(method_name)
162
+ # Compatibility: Old import hook took 2 arguments.
163
+ # Warn and call with a blank hash as 2nd argument.
164
+ if object.method(method_name).arity == 2
165
+ report_old_import_hook(method_name)
166
+ object.send(method_name, record, {})
167
+ else
168
+ object.send(method_name, record)
169
+ end
170
+ end
171
+ end
172
+
173
+ def report_old_import_hook(method_name)
174
+ unless @old_import_hook_reported
175
+ error = I18n.t("admin.import.import_error.old_import_hook",
176
+ model: import_model.display_name,
177
+ method: method_name)
178
+ report_general_error(error)
179
+ @old_import_hook_reported = true
180
+ end
181
+ end
182
+
183
+ def perform_global_callback(method_name)
184
+ object = import_model.model
185
+ object.send(method_name) if object.respond_to?(method_name)
186
+ end
187
+
188
+ def find_or_create_object(record, update)
189
+ field_names = import_model.model_fields.map(&:name)
190
+ new_attrs = record.select do |field_name, value|
191
+ field_names.include?(field_name) && !value.blank?
192
+ end
193
+
194
+ model = import_model.model
195
+ object = if update.present?
196
+ query = update.each_with_object({}) do
197
+ |field, query| query[field] = record[field]
198
+ end
199
+ model.where(query).first
200
+ end
201
+
202
+ if object.nil?
203
+ object = model.new
204
+ perform_model_callback(object, :before_import_attributes, record)
205
+ object.attributes = new_attrs
206
+ else
207
+ perform_model_callback(object, :before_import_attributes, record)
208
+ object.attributes = new_attrs.except(update.map(&:to_sym))
209
+ end
210
+ object
211
+ end
212
+
213
+ def import_single_association_data(object, record)
214
+ import_model.single_association_fields.each do |field|
215
+ mapping_key = params[:associations][field.name]
216
+ value = extract_mapping(record[field.name], mapping_key)
217
+
218
+ if !value.blank?
219
+ object.send "#{field.name}=", import_model.associated_object(field, mapping_key, value)
220
+ end
221
+ end
222
+ end
223
+
224
+ def import_many_association_data(object, record)
225
+ import_model.many_association_fields.each do |field|
226
+ if record.has_key? field.name
227
+ mapping_key = params[:associations][field.name]
228
+ values = record[field.name].reject { |value| value.blank? }.map { |value|
229
+ extract_mapping(value, mapping_key)
230
+ }
231
+
232
+ if !values.empty?
233
+ associated = values.map { |value| import_model.associated_object(field, mapping_key, value) }
234
+ object.send "#{field.name}=", associated
235
+ end
236
+ end
237
+ end
238
+ end
239
+
240
+ def extract_mapping(value, mapping_key)
241
+ if value.is_a? Hash
242
+ value[mapping_key]
243
+ else
244
+ value
245
+ end
246
+ end
247
+ end
248
+ end