rails_admin_import 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []