active_admin_csv_import 1.1.0 → 1.1.1
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/Rakefile +9 -34
- data/app/assets/javascripts/active_admin_csv_import/import_csv.js +76 -26
- data/app/views/admin/csv/_import_csv_failed_row.html.erb +5 -0
- data/app/views/admin/csv/_instructions.html.erb +9 -0
- data/app/views/admin/csv/import_csv.html.erb +8 -9
- data/lib/active_admin_csv_import/dsl.rb +63 -3
- data/lib/active_admin_csv_import/version.rb +1 -1
- data/vendor/assets/javascripts/recline/backend.csv.js +9 -10
- metadata +9 -6
data/Rakefile
CHANGED
@@ -1,40 +1,15 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
rescue LoadError
|
5
|
-
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
6
|
-
end
|
7
|
-
begin
|
8
|
-
require 'rdoc/task'
|
9
|
-
rescue LoadError
|
10
|
-
require 'rdoc/rdoc'
|
11
|
-
require 'rake/rdoctask'
|
12
|
-
RDoc::Task = Rake::RDocTask
|
13
|
-
end
|
14
|
-
|
15
|
-
RDoc::Task.new(:rdoc) do |rdoc|
|
16
|
-
rdoc.rdoc_dir = 'rdoc'
|
17
|
-
rdoc.title = 'ActiveAdminCsvImport'
|
18
|
-
rdoc.options << '--line-numbers'
|
19
|
-
rdoc.rdoc_files.include('README.rdoc')
|
20
|
-
rdoc.rdoc_files.include('lib/**/*.rb')
|
21
|
-
end
|
22
|
-
|
23
|
-
APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__)
|
24
|
-
load 'rails/tasks/engine.rake'
|
25
|
-
|
26
|
-
|
27
|
-
|
1
|
+
require "bundler"
|
2
|
+
require 'rake'
|
3
|
+
Bundler.setup
|
28
4
|
Bundler::GemHelper.install_tasks
|
29
5
|
|
30
|
-
require 'rake/testtask'
|
31
6
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
t.pattern = 'test/**/*_test.rb'
|
36
|
-
t.verbose = false
|
7
|
+
def cmd(command)
|
8
|
+
puts command
|
9
|
+
raise unless system command
|
37
10
|
end
|
38
11
|
|
12
|
+
# require File.expand_path('../spec/support/detect_rails_version', __FILE__)
|
39
13
|
|
40
|
-
|
14
|
+
# Import all our rake tasks
|
15
|
+
FileList['tasks/**/*.rake'].each { |task| import task }
|
@@ -20,8 +20,12 @@ $(document).ready(function() {
|
|
20
20
|
$($file).wrap('<form>').closest('form').get(0).reset();
|
21
21
|
$($file).unwrap();
|
22
22
|
|
23
|
+
// Clear progress
|
23
24
|
var progress = $("#csv-import-progress");
|
24
25
|
progress.text("");
|
26
|
+
|
27
|
+
// Clear validation errors
|
28
|
+
$("#csv-import-errors").html("");
|
25
29
|
};
|
26
30
|
|
27
31
|
// listen for the file to be submitted
|
@@ -33,6 +37,7 @@ $(document).ready(function() {
|
|
33
37
|
// create the dataset in the usual way but specifying file attribute
|
34
38
|
var dataset = new recline.Model.Dataset({
|
35
39
|
file: $file.files[0],
|
40
|
+
delimiter: import_csv_delimiter,
|
36
41
|
backend: 'csv'
|
37
42
|
});
|
38
43
|
|
@@ -44,19 +49,22 @@ $(document).ready(function() {
|
|
44
49
|
return;
|
45
50
|
}
|
46
51
|
|
47
|
-
// Re-query to
|
48
|
-
dataset.query({
|
52
|
+
// Re-query to just one record so we can check the headers.
|
53
|
+
dataset.query({
|
54
|
+
size: 1
|
55
|
+
});
|
49
56
|
|
50
57
|
// Check whether the CSV's columns match up with our data model.
|
51
58
|
// import_csv_fields is passed in from Rails in import_csv.html.erb
|
52
|
-
var
|
59
|
+
var required_columns = import_csv_required_columns;
|
60
|
+
var all_columns = import_csv_columns;
|
53
61
|
var csv_columns = _.pluck(data.records.first().fields.models, "id");
|
54
62
|
var normalised_csv_columns = _.map(csv_columns, function(name) {
|
55
63
|
return _.underscored(name);
|
56
64
|
});
|
57
65
|
|
58
66
|
// Check we have all the columns we want.
|
59
|
-
var missing_columns = _.difference(
|
67
|
+
var missing_columns = _.difference(required_columns, normalised_csv_columns);
|
60
68
|
var missing_columns_humanized = _.map(missing_columns, function(name) {
|
61
69
|
return _.humanize(name);
|
62
70
|
});
|
@@ -71,48 +79,90 @@ $(document).ready(function() {
|
|
71
79
|
var succeeded = 0;
|
72
80
|
var i = 0;
|
73
81
|
|
74
|
-
|
82
|
+
// Batch rows into 50s to send to the server
|
83
|
+
// var n = 50;
|
84
|
+
// var batchedModels = _.groupBy(data.records.models, function(a, b) {
|
85
|
+
// return Math.floor(b / n);
|
86
|
+
// });
|
87
|
+
|
88
|
+
var rowIndex = 0;
|
89
|
+
|
90
|
+
var postRows = function(dataset, index) {
|
75
91
|
|
76
|
-
//
|
77
|
-
|
78
|
-
|
92
|
+
// Query the data set for the next batch of rows.
|
93
|
+
dataset.query({
|
94
|
+
size: 100,
|
95
|
+
from: 100 * index
|
96
|
+
});
|
97
|
+
var currentBatch = data.records.models;
|
98
|
+
|
99
|
+
var records_data = [];
|
100
|
+
|
101
|
+
// Construct the payload for each row
|
102
|
+
_.each(currentBatch, function(record, i) {
|
79
103
|
|
80
104
|
// Filter only the attributes we want, and normalise column names.
|
81
105
|
var record_data = {};
|
82
|
-
record_data[
|
106
|
+
record_data["_row"] = rowIndex;
|
83
107
|
|
108
|
+
// Construct the resource params with underscored keys
|
84
109
|
_.each(_.pairs(record.attributes), function(attr) {
|
85
110
|
var underscored_name = _.underscored(attr[0]);
|
86
|
-
if (_.contains(
|
87
|
-
|
111
|
+
if (_.contains(all_columns, underscored_name)) {
|
112
|
+
|
113
|
+
var value = attr[1];
|
114
|
+
|
115
|
+
// Prevent null values coming through as string 'null' so allow_blank works on validations.
|
116
|
+
if (value === null) {
|
117
|
+
value = '';
|
118
|
+
}
|
119
|
+
|
120
|
+
record_data[underscored_name] = value;
|
88
121
|
}
|
89
122
|
});
|
90
123
|
|
91
|
-
|
124
|
+
records_data.push(record_data);
|
125
|
+
rowIndex = rowIndex + 1;
|
126
|
+
});
|
127
|
+
|
128
|
+
var payload = {};
|
129
|
+
payload[import_csv_resource_name] = records_data;
|
130
|
+
|
131
|
+
// Send this batch to the server.
|
132
|
+
$.post(
|
92
133
|
import_csv_path,
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
loaded = loaded +
|
134
|
+
payload,
|
135
|
+
null,
|
136
|
+
'json')
|
137
|
+
.always(function(xhr) {
|
138
|
+
loaded = loaded + currentBatch.length;
|
98
139
|
progress.text("Progress: " + loaded + " of " + total);
|
99
140
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
141
|
+
// Show validation errors for any failed rows.
|
142
|
+
$("#csv-import-errors").append(xhr.responseText);
|
143
|
+
|
144
|
+
if (xhr.status == 200) {
|
145
|
+
if (loaded == total) {
|
146
|
+
progress.html("Done. Imported " + total + " records.");
|
147
|
+
if (redirect_path) {
|
148
|
+
progress.html(progress.text() + " <a href='" + redirect_path + "'>Click to continue.</a>");
|
149
|
+
}
|
150
|
+
} else {
|
151
|
+
// Send the next batch!
|
152
|
+
postRows(dataset, index + 1);
|
104
153
|
}
|
154
|
+
} else {
|
155
|
+
alert("Import interrupted. The server could not be reached or encountered an error.");
|
105
156
|
}
|
106
|
-
});
|
107
157
|
|
108
|
-
|
158
|
+
});
|
159
|
+
};
|
109
160
|
|
110
|
-
i++;
|
111
161
|
|
112
|
-
|
162
|
+
postRows(dataset, 0);
|
113
163
|
}
|
114
164
|
|
115
165
|
clearFileInput();
|
116
166
|
});
|
117
167
|
});
|
118
|
-
});
|
168
|
+
});
|
@@ -0,0 +1,9 @@
|
|
1
|
+
<h3>
|
2
|
+
<%= t('.import_from_csv_file',
|
3
|
+
resource_name: active_admin_config.resource_name.to_s.pluralize.humanize,
|
4
|
+
default: 'Import %{resource_name} from a CSV File' ) %>
|
5
|
+
</h3>
|
6
|
+
<ul>
|
7
|
+
<li><%= t('.file_type', default: "Save a CSV as 'Windows Comma Separated' from Excel.") %></li>
|
8
|
+
<li><%= t('.must_have_headings', columns: @required_columns.to_sentence, default: "Your CSV should have the following column headings: %{columns}. The order doesn't matter.") %></li>
|
9
|
+
</ul>
|
@@ -1,18 +1,17 @@
|
|
1
1
|
<%= javascript_include_tag "active_admin_csv_import/import_csv" %>
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
<li>Save a CSV as 'Windows Comma Separated' from Excel.</li>
|
6
|
-
<li>Your CSV should have the following column headings: <%= @fields.map(&:to_s).map(&:humanize).to_sentence %>. The order doesn't matter.</li>
|
7
|
-
<li>If a record already exists a duplicate will be created.</li>
|
8
|
-
</ul>
|
3
|
+
<%= render partial: 'admin/csv/instructions' %>
|
4
|
+
|
9
5
|
<input id="csv-file-input" type="file">
|
10
6
|
|
11
7
|
<div id="csv-import-progress"></div>
|
8
|
+
<div id="csv-import-errors"></div>
|
12
9
|
|
13
10
|
<script type="text/javascript">
|
14
|
-
var
|
11
|
+
var import_csv_columns = <%= @columns.to_json.html_safe %>;
|
12
|
+
var import_csv_required_columns = <%= @required_columns.to_json.html_safe %>;
|
15
13
|
var import_csv_path = <%= @post_path.to_json.html_safe %>;
|
16
|
-
var import_csv_resource_name = <%= active_admin_config.resource_name.underscore.to_json.html_safe %>;
|
14
|
+
var import_csv_resource_name = <%= active_admin_config.resource_name.to_s.underscore.to_json.html_safe %>;
|
17
15
|
var redirect_path = <%= @redirect_path.to_json.html_safe %>;
|
18
|
-
|
16
|
+
var import_csv_delimiter = <%= @delimiter ? @delimiter.to_json.html_safe : "null" %>;
|
17
|
+
</script>
|
@@ -2,23 +2,83 @@ module ActiveAdminCsvImport
|
|
2
2
|
module DSL
|
3
3
|
|
4
4
|
def csv_importable(options={})
|
5
|
+
|
5
6
|
action_item :only => :index do
|
6
7
|
link_to "Import #{active_admin_config.resource_name.to_s.pluralize}", :action => 'import_csv'
|
7
8
|
end
|
8
9
|
|
10
|
+
# Shows the form and JS which accepts a CSV file, parses it and posts each row to the server.
|
9
11
|
collection_action :import_csv do
|
10
|
-
@
|
12
|
+
@columns = options[:columns] ||= active_admin_config.resource_class.columns.map(&:name) - ["id", "updated_at", "created_at"]
|
13
|
+
@required_columns = options[:required_columns] ||= @columns
|
11
14
|
|
12
15
|
@post_path = options[:path].try(:call)
|
13
|
-
@post_path ||= collection_path
|
16
|
+
@post_path ||= collection_path + "/import_rows"
|
14
17
|
|
15
18
|
@redirect_path = options[:redirect_path].try(:call)
|
16
19
|
@redirect_path ||= collection_path
|
17
20
|
|
21
|
+
@delimiter = options[:delimiter]
|
22
|
+
|
18
23
|
render "admin/csv/import_csv"
|
19
24
|
end
|
20
25
|
|
26
|
+
# Receives each row and saves it
|
27
|
+
collection_action :import_rows, :method => :post do
|
28
|
+
|
29
|
+
@failures = []
|
30
|
+
|
31
|
+
csv_resource_params.values.each do |row_params|
|
32
|
+
row_params = row_params.with_indifferent_access
|
33
|
+
row_number = row_params.delete('_row')
|
34
|
+
|
35
|
+
resource = existing_row_resource(options[:import_unique_key], row_params)
|
36
|
+
resource ||= active_admin_config.resource_class.new()
|
37
|
+
|
38
|
+
if not update_row_resource(resource, row_params)
|
39
|
+
@failures << {
|
40
|
+
row_number: row_number,
|
41
|
+
resource: resource
|
42
|
+
}
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
render :partial => "admin/csv/import_csv_failed_row", :status => 200
|
47
|
+
end
|
48
|
+
|
49
|
+
# Rails 4 Strong Parameters compatibility and backwards compatibility.
|
50
|
+
controller do
|
51
|
+
def csv_resource_params
|
52
|
+
# I don't think this will work any more.
|
53
|
+
if respond_to?(:permitted_params)
|
54
|
+
permitted_params[active_admin_config.resource_class.name.underscore]
|
55
|
+
else
|
56
|
+
params[active_admin_config.resource_class.name.underscore]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Updates a resource with the CSV data and saves it.
|
61
|
+
#
|
62
|
+
# @param resource [Object] the object to save
|
63
|
+
# @param params [Hash] the CSV row
|
64
|
+
# @return [Boolean] Success
|
65
|
+
def update_row_resource(resource, params)
|
66
|
+
resource.attributes = params
|
67
|
+
resource.save
|
68
|
+
end
|
69
|
+
|
70
|
+
def existing_row_resource(lookup_column, params)
|
71
|
+
return unless lookup_column
|
72
|
+
|
73
|
+
finder_method = "find_by_#{lookup_column}".to_sym
|
74
|
+
value = params[lookup_column]
|
75
|
+
return unless value.present?
|
76
|
+
|
77
|
+
return active_admin_config.resource_class.send(finder_method, value)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
21
81
|
end
|
22
82
|
|
23
83
|
end
|
24
|
-
end
|
84
|
+
end
|
@@ -27,7 +27,7 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {};
|
|
27
27
|
useMemoryStore: true
|
28
28
|
});
|
29
29
|
};
|
30
|
-
reader.onerror = function
|
30
|
+
reader.onerror = function(e) {
|
31
31
|
alert('Failed to load file. Code: ' + e.target.error.code);
|
32
32
|
};
|
33
33
|
reader.readAsText(dataset.file, encoding);
|
@@ -56,14 +56,14 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {};
|
|
56
56
|
//
|
57
57
|
// @return The CSV parsed as an array
|
58
58
|
// @type Array
|
59
|
-
//
|
59
|
+
//
|
60
60
|
// @param {String} s The string to convert
|
61
61
|
// @param {Object} options Options for loading CSV including
|
62
62
|
// @param {Boolean} [trim=false] If set to True leading and trailing whitespace is stripped off of each non-quoted field as it is imported
|
63
63
|
// @param {String} [separator=','] Separator for CSV file
|
64
64
|
// Heavily based on uselesscode's JS CSV parser (MIT Licensed):
|
65
65
|
// http://www.uselesscode.org/javascript/csv/
|
66
|
-
my.parseCSV= function(s, options) {
|
66
|
+
my.parseCSV = function(s, options) {
|
67
67
|
// Get rid of any trailing \n
|
68
68
|
s = chomp(s);
|
69
69
|
|
@@ -71,7 +71,6 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {};
|
|
71
71
|
var trm = (options.trim === false) ? false : true;
|
72
72
|
var separator = options.separator || ',';
|
73
73
|
var delimiter = options.delimiter || '"';
|
74
|
-
|
75
74
|
var cur = '', // The character we are currently processing.
|
76
75
|
inQuote = false,
|
77
76
|
fieldQuoted = false,
|
@@ -81,12 +80,12 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {};
|
|
81
80
|
i,
|
82
81
|
processField;
|
83
82
|
|
84
|
-
processField = function
|
83
|
+
processField = function(field) {
|
85
84
|
if (fieldQuoted !== true) {
|
86
85
|
// If field is empty set to null
|
87
86
|
if (field === '') {
|
88
87
|
field = null;
|
89
|
-
|
88
|
+
// If the field was not quoted and we are trimming fields, trim it
|
90
89
|
} else if (trm === true) {
|
91
90
|
field = trim(field);
|
92
91
|
}
|
@@ -106,7 +105,7 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {};
|
|
106
105
|
|
107
106
|
// If we are at a EOF or EOR
|
108
107
|
if (inQuote === false && (cur === separator || cur === "\n")) {
|
109
|
-
|
108
|
+
field = processField(field);
|
110
109
|
// Add the current field to the current row
|
111
110
|
row.push(field);
|
112
111
|
// If this is EOR append row to output and flush row
|
@@ -155,14 +154,14 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {};
|
|
155
154
|
// contains a comma double quote or a newline
|
156
155
|
// it needs to be quoted in CSV output
|
157
156
|
rxNeedsQuoting = /^\s|\s$|,|"|\n/,
|
158
|
-
trim = (function
|
157
|
+
trim = (function() {
|
159
158
|
// Fx 3.1 has a native trim function, it's about 10x faster, use it if it exists
|
160
159
|
if (String.prototype.trim) {
|
161
|
-
return function
|
160
|
+
return function(s) {
|
162
161
|
return s.trim();
|
163
162
|
};
|
164
163
|
} else {
|
165
|
-
return function
|
164
|
+
return function(s) {
|
166
165
|
return s.replace(/^\s*/, '').replace(/\s*$/, '');
|
167
166
|
};
|
168
167
|
}
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_admin_csv_import
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.1.
|
4
|
+
version: 1.1.1
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-
|
12
|
+
date: 2013-12-09 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rails
|
16
|
-
requirement: &
|
16
|
+
requirement: &70147043295120 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: '3.1'
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *70147043295120
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: railties
|
27
|
-
requirement: &
|
27
|
+
requirement: &70147043307680 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ! '>='
|
@@ -32,7 +32,7 @@ dependencies:
|
|
32
32
|
version: '3.1'
|
33
33
|
type: :runtime
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *70147043307680
|
36
36
|
description: CSV import for Active Admin capable of handling CSV files too large to
|
37
37
|
import via direct file upload to Heroku
|
38
38
|
email:
|
@@ -42,6 +42,8 @@ extensions: []
|
|
42
42
|
extra_rdoc_files: []
|
43
43
|
files:
|
44
44
|
- app/assets/javascripts/active_admin_csv_import/import_csv.js
|
45
|
+
- app/views/admin/csv/_import_csv_failed_row.html.erb
|
46
|
+
- app/views/admin/csv/_instructions.html.erb
|
45
47
|
- app/views/admin/csv/import_csv.html.erb
|
46
48
|
- lib/active_admin_csv_import/dsl.rb
|
47
49
|
- lib/active_admin_csv_import/engine.rb
|
@@ -83,3 +85,4 @@ signing_key:
|
|
83
85
|
specification_version: 3
|
84
86
|
summary: Add CSV import to Active Admin
|
85
87
|
test_files: []
|
88
|
+
has_rdoc:
|