rails_admin_import 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.
data/README.md ADDED
@@ -0,0 +1,61 @@
1
+ Rails Admin Import functionality
2
+ ========
3
+
4
+ Plugin functionality to add generic import to Rails Admin interface
5
+
6
+ Installation
7
+ ========
8
+
9
+ * First, add to Gemfile:
10
+
11
+ gem "rails_admin_import", :git => "git://github.com/stephskardal/demo.git"
12
+
13
+ * Next, mount in your application by adding:
14
+
15
+ mount RailsAdminImport::Engine => '/rails_admin_import', :as => 'rails_admin_import'" to config/routes
16
+
17
+ * Add to cancan to allow access to import:
18
+
19
+ can :import, [User, Model1, Model2]
20
+
21
+ * Define configuration:
22
+
23
+ RailsAdminImport.config do |config|
24
+ config.model User do
25
+ excluded_fields do
26
+ [:field1, :field2, :field3]
27
+ end
28
+ label :name
29
+ extra_fields do
30
+ [:field3, :field4, :field5]
31
+ end
32
+ end
33
+ end
34
+
35
+ * (Optional) Define instance methods to be hooked into the import process, if special/additional processing is required on the data:
36
+
37
+ # some model
38
+ def before_import_save(row, map)
39
+ self.set_permalink
40
+ self.import_nested_data(row, map)
41
+ end
42
+
43
+ * "import" action must be added inside config.actions block in main application RailsAdmin configuration.
44
+
45
+ config.actions do
46
+ ...
47
+ import
48
+
49
+ end
50
+
51
+ Refer to RailAdmin documentation on custom actions that must be present in this block.
52
+
53
+ TODO
54
+ ========
55
+
56
+ * Testing
57
+
58
+ Copyright
59
+ ========
60
+
61
+ Copyright (c) 2011 End Point & Steph Skardal. See LICENSE.txt for further details.
@@ -0,0 +1,102 @@
1
+ <% if @response -%>
2
+ <% if @response.has_key?(:error) -%>
3
+ <div class="alert-message error">
4
+ <%= @response[:error] %>
5
+ </div>
6
+ <% end -%>
7
+ <% if @response.has_key?(:notice) -%>
8
+ <div class="alert-message notice">
9
+ <%= @response[:notice] %>
10
+ </div>
11
+ <% end -%>
12
+ <% end -%>
13
+
14
+ <h1>Import <%= @abstract_model.to_param.titleize %></h1>
15
+
16
+ <small>The following fields may be included in the import file</small>
17
+ <table width="100%" cellpadding="0" cellspacing="0">
18
+ <tr>
19
+ <td width="20%" valign="top">
20
+ <h3>Standard Fields</h3>
21
+ <ul>
22
+ <% @abstract_model.model.import_fields.each do |field| -%>
23
+ <li><%= field %></li>
24
+ <% end -%>
25
+ </ul>
26
+ </td>
27
+
28
+ <% if @abstract_model.model.belongs_to_fields.any? -%>
29
+ <td width="20%" valign="top">
30
+ <h3>Belongs To Fields</h3>
31
+ <ul>
32
+ <% @abstract_model.model.belongs_to_fields.each do |field| -%>
33
+ <li><%= field %></li>
34
+ <% end -%>
35
+ </ul>
36
+ <small>These fields map to other items in the database, lookup via attribute selected below.</small>
37
+ </td>
38
+ <% end -%>
39
+
40
+ <% if @abstract_model.model.file_fields.any? -%>
41
+ <td width="20%" valign="top">
42
+ <h3>File Fields</h3>
43
+ <ul>
44
+ <% @abstract_model.model.file_fields.each do |field| -%>
45
+ <li><%= field %></li>
46
+ <% end -%>
47
+ </ul>
48
+ <small>These must be a downloadable URL.</small>
49
+ </td>
50
+ <% end -%>
51
+
52
+ <% if @abstract_model.model.many_fields.any? -%>
53
+ <td width="20%" valign="top">
54
+ <h3>Multiple Fields</h3>
55
+ <ul>
56
+ <% @abstract_model.model.many_fields.each do |field| -%>
57
+ <li><%= field %></li>
58
+ <% end -%>
59
+ </ul>
60
+ <small>These fields map to other columns in the database, lookup via attribute selected below. There may be multiple columns with this header in the spreadsheet.</small>
61
+ </td>
62
+ <% end -%>
63
+
64
+ <% if RailsAdminImport.config(@abstract_model.model).extra_fields.any? -%>
65
+ <td width="20%" valign="top">
66
+ <h3>Extra Fields</h3>
67
+ <ul>
68
+ <% RailsAdminImport.config(@abstract_model.model).extra_fields.each do |field| -%>
69
+ <li><%= field %></li>
70
+ <% end -%>
71
+ </ul>
72
+ <small>Additional application specific fields.</small>
73
+ </td>
74
+ <% end -%>
75
+ </tr>
76
+ </table>
77
+
78
+ <%= form_tag rails_admin.import_url(@abstract_model.to_param), :multipart => true do |f| -%>
79
+ <%= file_field_tag :file %>
80
+
81
+ <p>
82
+ <%= check_box_tag :update_if_exists %> Update if Exists<br />
83
+ Update lookup field
84
+ <select name="update_lookup">
85
+ <% @abstract_model.model.new.attributes.keys.each do |key| -%>
86
+ <option value="<%= key %>"><%= key %></option>
87
+ <% end -%>
88
+ </select>
89
+ </p>
90
+
91
+ <% [@abstract_model.model.belongs_to_fields, @abstract_model.model.many_fields].flatten.each do |field| -%>
92
+ <div style="display:inline-block; width: 45%;background:#cecece;margin: 5px;padding: 5px;">
93
+ <label style="width:200px;"><%= field %> mapping</label>&nbsp;&nbsp;
94
+ <select name="<%= field %>">
95
+ <% field.to_s.classify.constantize.new.attributes.keys.each do |key| -%>
96
+ <option value="<%= key %>"><%= key %></option>
97
+ <% end -%>
98
+ <select>
99
+ </div>
100
+ <% end -%><br />
101
+ <%= submit_tag "Upload", :disable_with => "Uploading..." %>
102
+ <% end -%>
@@ -0,0 +1,11 @@
1
+
2
+ en:
3
+ admin:
4
+ actions:
5
+ import:
6
+ title: "Import"
7
+ menu: "Import"
8
+ breadcrumb: "Import"
9
+ link: "Import"
10
+ bulk_link: "Import"
11
+ done: "Imported"
@@ -0,0 +1,57 @@
1
+ require "rails_admin_import/engine"
2
+ require "rails_admin_import/import"
3
+ require "rails_admin_import/config"
4
+
5
+ module RailsAdminImport
6
+ def self.config(entity = nil, &block)
7
+ if entity
8
+ RailsAdminImport::Config.model(entity, &block)
9
+ elsif block_given? && ENV['SKIP_RAILS_ADMIN_INITIALIZER'] != "true"
10
+ block.call(RailsAdminImport::Config)
11
+ else
12
+ RailsAdminImport::Config
13
+ end
14
+ end
15
+
16
+ def self.reset
17
+ RailsAdminImport::Config.reset
18
+ end
19
+ end
20
+
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
@@ -0,0 +1,52 @@
1
+ require 'rails_admin_import/config/model'
2
+ # require 'active_support/core_ext/class/attribute_accessors'
3
+
4
+ module RailsAdminImport
5
+ module Config
6
+ class << self
7
+ # Stores model configuration objects in a hash identified by model's class
8
+ # name.
9
+ #
10
+ # @see RailsAdminImport::Config.model
11
+ attr_reader :registry
12
+
13
+ # Loads a model configuration instance from the registry or registers
14
+ # a new one if one is yet to be added.
15
+ #
16
+ # First argument can be an instance of requested model, its class object,
17
+ # its class name as a string or symbol or a RailsAdminImport::AbstractModel
18
+ # instance.
19
+ #
20
+ # If a block is given it is evaluated in the context of configuration instance.
21
+ #
22
+ # Returns given model's configuration
23
+ #
24
+ # @see RailsAdminImport::Config.registry
25
+ def model(entity, &block)
26
+ key = entity.name.to_sym
27
+
28
+ config = @registry[key] ||= RailsAdminImport::Config::Model.new(entity)
29
+ config.instance_eval(&block) if block
30
+ config
31
+ end
32
+
33
+ # Reset all configurations to defaults.
34
+ #
35
+ # @see RailsAdminImport::Config.registry
36
+ def reset
37
+ @registry = {}
38
+ end
39
+
40
+ # Reset a provided model's configuration.
41
+ #
42
+ # @see RailsAdminImport::Config.registry
43
+ def reset_model(model)
44
+ key = model.kind_of?(Class) ? model.name.to_sym : model.to_sym
45
+ @registry.delete(key)
46
+ end
47
+ end
48
+
49
+ # Set default values for configuration options on load
50
+ self.reset
51
+ end
52
+ end
@@ -0,0 +1,62 @@
1
+ module RailsAdminImport
2
+ module Config
3
+ class Base
4
+ def initialize(parent = nil)
5
+ end
6
+
7
+ # Register an instance option for this object only
8
+ def register_instance_option(option_name, &default)
9
+ scope = class << self; self; end;
10
+ self.class.register_instance_option(option_name, scope, &default)
11
+ end
12
+
13
+ # Register an instance option. Instance option is a configuration
14
+ # option that stores its value within an instance variable and is
15
+ # accessed by an instance method. Both go by the name of the option.
16
+ def self.register_instance_option(option_name, scope = self, &default)
17
+ unless options = scope.instance_variable_get("@config_options")
18
+ options = scope.instance_variable_set("@config_options", {})
19
+ end
20
+
21
+ option_name = option_name.to_s
22
+
23
+ options[option_name] = nil
24
+
25
+ # If it's a boolean create an alias for it and remove question mark
26
+ if "?" == option_name[-1, 1]
27
+ scope.send(:define_method, "#{option_name.chop!}?") do
28
+ send(option_name)
29
+ end
30
+ end
31
+
32
+ # Define getter/setter by the option name
33
+ scope.send(:define_method, option_name) do |*args, &block|
34
+ if !args[0].nil? || block
35
+ # Invocation with args --> This is the declaration of the option, i.e. setter
36
+ instance_variable_set("@#{option_name}_registered", args[0].nil? ? block : args[0])
37
+ else
38
+ # Invocation without args nor block --> It's the use of the option, i.e. getter
39
+ value = instance_variable_get("@#{option_name}_registered")
40
+ case value
41
+ when Proc
42
+ # Track recursive invocation with an instance variable. This prevents run-away recursion
43
+ # and allows configurations such as
44
+ # label { "#{label}".upcase }
45
+ # This will use the default definition when called recursively.
46
+ if instance_variable_get("@#{option_name}_recurring")
47
+ value = instance_eval &default
48
+ else
49
+ instance_variable_set("@#{option_name}_recurring", true)
50
+ value = instance_eval &value
51
+ instance_variable_set("@#{option_name}_recurring", false)
52
+ end
53
+ when nil
54
+ value = instance_eval &default
55
+ end
56
+ value
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,23 @@
1
+ require 'rails_admin_import/config'
2
+ require 'rails_admin_import/config/base'
3
+
4
+ module RailsAdminImport
5
+ module Config
6
+ class Model < RailsAdminImport::Config::Base
7
+ def initialize(entity)
8
+ end
9
+
10
+ register_instance_option(:label) do
11
+ :id
12
+ end
13
+
14
+ register_instance_option(:excluded_fields) do
15
+ []
16
+ end
17
+
18
+ register_instance_option(:extra_fields) do
19
+ []
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,4 @@
1
+ module RailsAdminImport
2
+ class Engine < Rails::Engine
3
+ end
4
+ end
@@ -0,0 +1,186 @@
1
+ require 'open-uri'
2
+
3
+ module RailsAdminImport
4
+ module Import
5
+ extend ActiveSupport::Concern
6
+
7
+ module ClassMethods
8
+ def file_fields
9
+ if self.methods.include?(:attachment_definitions) && !self.attachment_definitions.nil?
10
+ return self.attachment_definitions.keys
11
+ end
12
+ []
13
+ end
14
+
15
+ def import_fields
16
+ fields = []
17
+
18
+ fields = self.new.attributes.keys.collect { |key| key.to_sym }
19
+
20
+ self.belongs_to_fields.each do |key|
21
+ fields.delete("#{key}_id".to_sym)
22
+ end
23
+
24
+ self.file_fields.each do |key|
25
+ fields.delete("#{key}_file_name".to_sym)
26
+ fields.delete("#{key}_content_type".to_sym)
27
+ fields.delete("#{key}_file_size".to_sym)
28
+ fields.delete("#{key}_updated_at".to_sym)
29
+ end
30
+
31
+ excluded_fields = RailsAdminImport.config(self).excluded_fields
32
+ [:id, :created_at, :updated_at, excluded_fields].flatten.each do |key|
33
+ fields.delete(key)
34
+ end
35
+
36
+ fields
37
+ end
38
+
39
+ def belongs_to_fields
40
+ attrs = self.reflections.select { |k, v| v.macro == :belongs_to }.keys
41
+ attrs - RailsAdminImport.config(self).excluded_fields
42
+ end
43
+
44
+ def many_fields
45
+ attrs = []
46
+ self.reflections.each do |k, v|
47
+ if [:has_and_belongs_to_many, :has_many].include?(v.macro)
48
+ attrs << k.to_s.singularize.to_sym
49
+ end
50
+ end
51
+
52
+ attrs - RailsAdminImport.config(self).excluded_fields
53
+ end
54
+
55
+ def run_import(params)
56
+ if !params.has_key?(:file)
57
+ return results = { :success => [], :error => ["You must select a file."] }
58
+ end
59
+
60
+ file = CSV.new(params[:file].tempfile)
61
+ map = {}
62
+
63
+ file.readline.each_with_index do |key, i|
64
+ if self.many_fields.include?(key.to_sym)
65
+ map[key.to_sym] ||= []
66
+ map[key.to_sym] << i
67
+ else
68
+ map[key.to_sym] = i
69
+ end
70
+ end
71
+
72
+ results = { :success => [], :error => [] }
73
+
74
+ associated_map = {}
75
+ self.belongs_to_fields.flatten.each do |field|
76
+ associated_map[field] = field.to_s.classify.constantize.all.inject({}) { |hash, c| hash[c.send(params[field])] = c.id; hash }
77
+ end
78
+ self.many_fields.flatten.each do |field|
79
+ associated_map[field] = field.to_s.classify.constantize.all.inject({}) { |hash, c| hash[c.send(params[field])] = c; hash }
80
+ end
81
+
82
+ label_method = RailsAdminImport.config(self).label
83
+
84
+ update = params.has_key?(:update_if_exists) && params[:update_if_exists] ? params[:update_lookup].to_sym : nil
85
+
86
+ file.each do |row|
87
+ object = self.import_initialize(row, map, update)
88
+ object.import_belongs_to_data(associated_map, row, map)
89
+ object.import_many_data(associated_map, row, map)
90
+ object.before_import_save(row, map)
91
+
92
+ object.import_files(row, map)
93
+
94
+ verb = object.new_record? ? "Create" : "Update"
95
+ if object.errors.empty?
96
+ if object.save
97
+ results[:success] << "#{verb}d: #{object.send(label_method)}"
98
+ else
99
+ results[:error] << "Failed to #{verb}: #{object.send(label_method)}. Errors: #{object.errors.full_messages.join(', ')}."
100
+ end
101
+ else
102
+ results[:error] << "Errors before save: #{object.send(label_method)}. Errors: #{object.errors.full_messages.join(', ')}."
103
+ end
104
+ end
105
+
106
+ results
107
+ end
108
+
109
+ def import_initialize(row, map, update)
110
+ new_attrs = {}
111
+ self.import_fields.each do |key|
112
+ new_attrs[key] = row[map[key]] if map[key]
113
+ end
114
+
115
+ item = nil
116
+ if update.present?
117
+ item = self.send("find_by_#{update}", row[map[update]])
118
+ end
119
+
120
+ if item.nil?
121
+ item = self.new(new_attrs)
122
+ else
123
+ item.attributes = new_attrs.except(update.to_sym)
124
+ item.save
125
+ end
126
+
127
+ item
128
+ end
129
+ end
130
+
131
+ def before_import_save(*args)
132
+ # Meant to be overridden to do special actions
133
+ end
134
+
135
+ def import_display
136
+ self.id
137
+ end
138
+
139
+ def import_files(row, map)
140
+ if self.new_record? && self.valid?
141
+ self.class.file_fields.each do |key|
142
+ if map[key] && !row[map[key]].nil?
143
+ begin
144
+ # Strip file
145
+ row[map[key]] = row[map[key]].gsub(/\s+/, "")
146
+ format = row[map[key]].match(/[a-z0-9]+$/)
147
+ open("#{Rails.root}/tmp/#{self.permalink}.#{format}", 'wb') { |file| file << open(row[map[key]]).read }
148
+ self.send("#{key}=", File.open("#{Rails.root}/tmp/#{self.permalink}.#{format}"))
149
+ rescue Exception => e
150
+ self.errors.add(:base, "Import error: #{e.inspect}")
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
156
+
157
+ def import_belongs_to_data(associated_map, row, map)
158
+ self.class.belongs_to_fields.each do |key|
159
+ if map.has_key?(key) && row[map[key]] != ""
160
+ self.send("#{key}_id=", associated_map[key][row[map[key]]])
161
+ end
162
+ end
163
+ end
164
+
165
+ def import_many_data(associated_map, row, map)
166
+ self.class.many_fields.each do |key|
167
+ values = []
168
+
169
+ map[key] ||= []
170
+ map[key].each do |pos|
171
+ if row[pos] != "" && associated_map[key][row[pos]]
172
+ values << associated_map[key][row[pos]]
173
+ end
174
+ end
175
+
176
+ if values.any?
177
+ self.send("#{key.to_s.pluralize}=", values)
178
+ end
179
+ end
180
+ end
181
+ end
182
+ end
183
+
184
+ class ActiveRecord::Base
185
+ include RailsAdminImport::Import
186
+ end
@@ -0,0 +1,3 @@
1
+ module RailsAdminImport
2
+ VERSION = "0.0.1"
3
+ end
metadata ADDED
@@ -0,0 +1,55 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rails_admin_import
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Steph Skardal
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-07-05 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description:
15
+ email: steph@endpoint.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files:
19
+ - README.md
20
+ files:
21
+ - app/views/rails_admin/main/import.html.erb
22
+ - config/locales/import.en.yml
23
+ - lib/rails_admin_import.rb
24
+ - lib/rails_admin_import/config.rb
25
+ - lib/rails_admin_import/config/base.rb
26
+ - lib/rails_admin_import/config/model.rb
27
+ - lib/rails_admin_import/engine.rb
28
+ - lib/rails_admin_import/import.rb
29
+ - lib/rails_admin_import/version.rb
30
+ - README.md
31
+ homepage:
32
+ licenses: []
33
+ post_install_message:
34
+ rdoc_options: []
35
+ require_paths:
36
+ - lib
37
+ required_ruby_version: !ruby/object:Gem::Requirement
38
+ none: false
39
+ requirements:
40
+ - - ! '>='
41
+ - !ruby/object:Gem::Version
42
+ version: '0'
43
+ required_rubygems_version: !ruby/object:Gem::Requirement
44
+ none: false
45
+ requirements:
46
+ - - ! '>='
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ requirements: []
50
+ rubyforge_project:
51
+ rubygems_version: 1.8.11
52
+ signing_key:
53
+ specification_version: 3
54
+ summary: Import functionality for rails admin
55
+ test_files: []