rails_admin_import_no_encoding 0.1.0

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.
@@ -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