rails_admin_import 0.1.9 → 1.0.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,75 @@
1
+ require "csv"
2
+ require "rchardet"
3
+
4
+ module RailsAdminImport
5
+ module Formats
6
+ class CSVImporter < FileImporter
7
+ Formats.register(:csv, self)
8
+
9
+ # Default is to downcase headers and add underscores to convert into attribute names
10
+ HEADER_CONVERTER = lambda do |header|
11
+ header.parameterize.underscore
12
+ end
13
+
14
+ def initialize(import_model, params)
15
+ super
16
+ @encoding = params[:encoding]
17
+ @header_converter = RailsAdminImport.config.header_converter || HEADER_CONVERTER
18
+ end
19
+
20
+ # A method that yields a hash of attributes for each record to import
21
+ def each_record
22
+ CSV.foreach(filename, csv_options) do |row|
23
+ yield convert_to_attributes(row)
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def csv_options
30
+ {
31
+ headers: true,
32
+ header_converters: @header_converter
33
+ }.tap do |options|
34
+ add_encoding!(options)
35
+ end
36
+ end
37
+
38
+ def add_encoding!(options)
39
+ from_encoding =
40
+ if !@encoding.blank?
41
+ @encoding
42
+ else
43
+ detect_encoding
44
+ end
45
+
46
+ to_encoding = import_model.abstract_model.encoding
47
+ if from_encoding && from_encoding != to_encoding
48
+ options[:encoding] = "#{from_encoding}:#{to_encoding}"
49
+ end
50
+ end
51
+
52
+ def detect_encoding
53
+ charset = CharDet.detect File.read(filename)
54
+ if charset["confidence"] > 0.6
55
+ from_encoding = charset["encoding"]
56
+ from_encoding = "UTF-8" if from_encoding == "ascii"
57
+ end
58
+ from_encoding
59
+ end
60
+
61
+ def convert_to_attributes(row)
62
+ row.each_with_object({}) do |(field, value), record|
63
+ break if field.nil?
64
+ field = field.to_sym
65
+ if import_model.has_multiple_values?(field)
66
+ field = import_model.pluralize_field(field)
67
+ (record[field] ||= []) << value
68
+ else
69
+ record[field] = value
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -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,45 @@
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
+ copy_path = File.join(Rails.root, "log", "import", copy_filename)
41
+ FileUtils.copy(filename, copy_path)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,31 @@
1
+ module RailsAdminImport
2
+ module Formats
3
+ class JSONImporter < FileImporter
4
+ Formats.register(:json, self)
5
+
6
+ # A method that yields a hash of attributes for each record to import
7
+ def each_record
8
+ File.open(filename) do |file|
9
+ data = JSON.load(file)
10
+
11
+ if data.is_a? Hash
12
+ # Load array from root key
13
+ data = data[root_key]
14
+ end
15
+
16
+ if !data.is_a? Array
17
+ raise ArgumentError, I18n.t("admin.import.invalid_json", root_key: root_key)
18
+ end
19
+
20
+ data.each do |record|
21
+ yield record.symbolize_keys
22
+ end
23
+ end
24
+ end
25
+
26
+ def root_key
27
+ import_model.model.model_name.element.pluralize
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,28 @@
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 for(format, *args)
9
+ @registry.fetch(format.to_s, DummyImporter).new(*args)
10
+ end
11
+
12
+ def all
13
+ @registry.keys
14
+ end
15
+
16
+ def reset
17
+ @registry = {}
18
+ end
19
+ end
20
+
21
+ reset
22
+ end
23
+ end
24
+
25
+ require "rails_admin_import/formats/dummy_importer"
26
+ require "rails_admin_import/formats/file_importer"
27
+ require "rails_admin_import/formats/csv_importer"
28
+ require "rails_admin_import/formats/json_importer"
@@ -2,13 +2,16 @@ module RailsAdminImport
2
2
  class ImportLogger
3
3
  attr_reader :logger
4
4
 
5
- def initialize(log_file_name = 'rails_admin_import.log')
6
- @logger = Logger.new("#{Rails.root}/log/#{log_file_name}") if RailsAdminImport.config.logging
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
7
9
  end
8
10
 
9
11
  def info(message)
10
- @logger.info message if RailsAdminImport.config.logging
12
+ if RailsAdminImport.config.logging
13
+ @logger.info message
14
+ end
11
15
  end
12
-
13
16
  end
14
17
  end
@@ -0,0 +1,94 @@
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 belongs_to_fields
48
+ @belongs_to_fields ||= association_fields.select { |f|
49
+ !f.multiple?
50
+ }
51
+ end
52
+
53
+ def many_fields
54
+ @many_fields ||= association_fields.select {
55
+ |f| f.multiple?
56
+ }
57
+ end
58
+
59
+ def associated_object(field, mapping_field, value)
60
+ klass = association_class(field)
61
+ klass.where(mapping_field => value).first or
62
+ raise AssociationNotFound, "#{klass}.#{mapping_field} = #{value}"
63
+ end
64
+
65
+ def association_class(field)
66
+ field.association.klass
67
+ end
68
+
69
+ def associated_config(field)
70
+ field.associated_model_config.import
71
+ end
72
+
73
+ def associated_model_fields(field)
74
+ @associated_fields ||= {}
75
+ @associated_fields[field] ||= associated_config(field).visible_fields.select { |f|
76
+ !f.association?
77
+ }
78
+ end
79
+
80
+ def has_multiple_values?(field_name)
81
+ plural_name = pluralize_field(field_name)
82
+ many_fields.any? { |field| field.name == field_name || field.name == plural_name }
83
+ end
84
+
85
+ def pluralize_field(field_name)
86
+ @plural_fields ||= many_fields.map(&:name).each_with_object({}) { |name, h|
87
+ h[name.to_s.singularize.to_sym] = name
88
+ }
89
+ @plural_fields[field_name] || field_name
90
+ end
91
+ end
92
+ end
93
+
94
+
@@ -0,0 +1,224 @@
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
+ # TODO: re-implement file size check
20
+ # if file_check.readlines.size > RailsAdminImport.config.line_item_limit
21
+ # return results = { :success => [], :error => ["Please limit upload file to #{RailsAdminImport.config.line_item_limit} line items."] }
22
+ # end
23
+
24
+ with_transaction do
25
+ records.each do |record|
26
+ import_record(record)
27
+ end
28
+
29
+ rollback_if_error
30
+ end
31
+ rescue Exception => e
32
+ report_general_error("#{e} (#{e.backtrace.first})")
33
+ end
34
+
35
+ format_results
36
+ end
37
+
38
+ private
39
+
40
+ def init_results
41
+ @results = { :success => [], :error => [] }
42
+ end
43
+
44
+ def with_transaction(&block)
45
+ if RailsAdminImport.config.rollback_on_error &&
46
+ defined?(ActiveRecord)
47
+
48
+ ActiveRecord::Base.transaction &block
49
+ else
50
+ block.call
51
+ end
52
+ end
53
+
54
+ def rollback_if_error
55
+ if RailsAdminImport.config.rollback_on_error &&
56
+ defined?(ActiveRecord) &&
57
+ !results[:error].empty?
58
+
59
+ results[:success] = []
60
+ raise ActiveRecord::Rollback
61
+ end
62
+ end
63
+
64
+ def import_record(record)
65
+ if update_lookup && !record.has_key?(update_lookup)
66
+ raise UpdateLookupError, I18n.t("admin.import.missing_update_lookup")
67
+ end
68
+
69
+ object = find_or_create_object(record, update_lookup)
70
+ action = object.new_record? ? :create : :update
71
+
72
+ begin
73
+ import_belongs_to_data(object, record)
74
+ import_has_many_data(object, record)
75
+ rescue AssociationNotFound => e
76
+ error = I18n.t("admin.import.association_not_found", :error => e.to_s)
77
+ report_error(object, action, error)
78
+ return
79
+ end
80
+
81
+ perform_model_callback(object, :before_import_save, record)
82
+
83
+ if object.save
84
+ report_success(object, action)
85
+ perform_model_callback(object, :after_import_save, record)
86
+ else
87
+ report_error(object, action, object.errors.full_messages.join(", "))
88
+ end
89
+ end
90
+
91
+ def update_lookup
92
+ @update_lookup ||= if params[:update_if_exists] == "1"
93
+ params[:update_lookup].to_sym
94
+ end
95
+ end
96
+
97
+ attr_reader :results
98
+
99
+ def logger
100
+ @logger ||= ImportLogger.new
101
+ end
102
+
103
+ def report_success(object, action)
104
+ object_label = import_model.label_for_model(object)
105
+ message = I18n.t("admin.import.import_success.#{action}",
106
+ :name => object_label)
107
+ logger.info "#{Time.now}: #{message}"
108
+ results[:success] << message
109
+ end
110
+
111
+ def report_error(object, action, error)
112
+ object_label = import_model.label_for_model(object)
113
+ message = I18n.t("admin.import.import_error.#{action}",
114
+ :name => object_label,
115
+ :error => error)
116
+ logger.info "#{Time.now}: #{message}"
117
+ results[:error] << message
118
+ end
119
+
120
+ def report_general_error(error)
121
+ message = I18n.t("admin.import.import_error.general", :error => error)
122
+ logger.info "#{Time.now}: #{message}"
123
+ results[:error] << message
124
+ end
125
+
126
+ def format_results
127
+ imported = results[:success]
128
+ not_imported = results[:error]
129
+ unless imported.empty?
130
+ results[:success_message] = format_result_message("successful", imported)
131
+ end
132
+ unless not_imported.empty?
133
+ results[:error_message] = format_result_message("error", not_imported)
134
+ end
135
+
136
+ results
137
+ end
138
+
139
+ def format_result_message(type, array)
140
+ result_count = "#{array.size} #{import_model.display_name.pluralize(array.size)}"
141
+ I18n.t("admin.flash.#{type}",
142
+ name: result_count,
143
+ action: I18n.t("admin.actions.import.done"))
144
+ end
145
+
146
+ def perform_model_callback(object, method_name, record)
147
+ if object.respond_to?(method_name)
148
+ # Compatibility: Old import hook took 2 arguments.
149
+ # Warn and call with a blank hash as 2nd argument.
150
+ if object.method(method_name).arity == 2
151
+ report_old_import_hook(method_name)
152
+ object.send(method_name, record, {})
153
+ else
154
+ object.send(method_name, record)
155
+ end
156
+ end
157
+ end
158
+
159
+ def report_old_import_hook(method_name)
160
+ unless @old_import_hook_reported
161
+ error = I18n.t("admin.import.import_error.old_import_hook",
162
+ model: import_model.display_name,
163
+ method: method_name)
164
+ report_general_error(error)
165
+ @old_import_hook_reported = true
166
+ end
167
+ end
168
+
169
+ def find_or_create_object(record, update)
170
+ field_names = import_model.model_fields.map(&:name)
171
+ new_attrs = record.select do |field_name, value|
172
+ field_names.include?(field_name) && !value.blank?
173
+ end
174
+
175
+ model = import_model.model
176
+ object = if update.present?
177
+ model.find_by(update => record[update])
178
+ end
179
+
180
+ if object.nil?
181
+ object = model.new(new_attrs)
182
+ else
183
+ object.attributes = new_attrs.except(update.to_sym)
184
+ end
185
+ object
186
+ end
187
+
188
+ def import_belongs_to_data(object, record)
189
+ import_model.belongs_to_fields.each do |field|
190
+ mapping_key = params[:associations][field.name]
191
+ value = extract_mapping(record[field.name], mapping_key)
192
+
193
+ if !value.blank?
194
+ object.send "#{field.name}=", import_model.associated_object(field, mapping_key, value)
195
+ end
196
+ end
197
+ end
198
+
199
+ def import_has_many_data(object, record)
200
+ import_model.many_fields.each do |field|
201
+ if record.has_key? field.name
202
+ mapping_key = params[:associations][field.name]
203
+ values = record[field.name].reject { |value| value.blank? }.map { |value|
204
+ extract_mapping(value, mapping_key)
205
+ }
206
+
207
+ if !values.empty?
208
+ associated = values.map { |value| import_model.associated_object(field, mapping_key, value) }
209
+ object.send "#{field.name}=", associated
210
+ end
211
+ end
212
+ end
213
+ end
214
+
215
+ def extract_mapping(value, mapping_key)
216
+ if value.is_a? Hash
217
+ value[mapping_key]
218
+ else
219
+ value
220
+ end
221
+ end
222
+ end
223
+ end
224
+
@@ -0,0 +1,23 @@
1
+ # Load the Rails Admin gem if not already done
2
+ require "rails_admin"
3
+
4
+ # Add the Import action
5
+ require "rails_admin_import/action"
6
+
7
+ # Add the import configuration section for models
8
+ require "rails_admin_import/config/sections/import"
9
+
10
+ # Register the configuration adapter for Rails Admin
11
+ # to allow configure_with(:import)
12
+ module RailsAdminImport
13
+ module Extension
14
+ class ConfigurationAdapter < SimpleDelegator
15
+ def initialize
16
+ super RailsAdminImport::Config
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ RailsAdmin.add_extension :import, RailsAdminImport::Extension,
23
+ configuration: true
@@ -1,3 +1,3 @@
1
1
  module RailsAdminImport
2
- VERSION = "0.1.9"
2
+ VERSION = "1.0.0"
3
3
  end
@@ -1,12 +1,15 @@
1
1
  require "rails_admin_import/engine"
2
- require "rails_admin_import/import"
2
+ require "rails_admin_import/import_model"
3
+ require "rails_admin_import/formats"
4
+ require "rails_admin_import/importer"
3
5
  require "rails_admin_import/config"
6
+ require "rails_admin_import/rails_admin_plugin"
4
7
 
5
8
  module RailsAdminImport
6
9
  def self.config(entity = nil, &block)
7
10
  if entity
8
11
  RailsAdminImport::Config.model(entity, &block)
9
- elsif block_given? && ENV['SKIP_RAILS_ADMIN_INITIALIZER'] != "true"
12
+ elsif block_given? && ENV["SKIP_RAILS_ADMIN_INITIALIZER"] != "true"
10
13
  block.call(RailsAdminImport::Config)
11
14
  else
12
15
  RailsAdminImport::Config
@@ -18,40 +21,3 @@ module RailsAdminImport
18
21
  end
19
22
  end
20
23
 
21
- require 'rails_admin/config/actions'
22
-
23
- module RailsAdmin
24
- module Config
25
- module Actions
26
- class Import < Base
27
- RailsAdmin::Config::Actions.register(self)
28
-
29
- register_instance_option :collection do
30
- true
31
- end
32
-
33
- register_instance_option :http_methods do
34
- [:get, :post]
35
- end
36
-
37
- register_instance_option :controller do
38
- Proc.new do
39
- @response = {}
40
-
41
- if request.post?
42
- results = @abstract_model.model.run_import(params)
43
- @response[:notice] = results[:success].join("<br />").html_safe if results[:success].any?
44
- @response[:error] = results[:error].join("<br />").html_safe if results[:error].any?
45
- end
46
-
47
- render :action => @action.template_name
48
- end
49
- end
50
-
51
- register_instance_option :link_icon do
52
- 'icon-folder-open'
53
- end
54
- end
55
- end
56
- end
57
- end