active_admin_csv_import 1.1.0 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -1,40 +1,15 @@
1
- #!/usr/bin/env rake
2
- begin
3
- require 'bundler/setup'
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
- Rake::TestTask.new(:test) do |t|
33
- t.libs << 'lib'
34
- t.libs << 'test'
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
- task :default => :test
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 get all records. Otherwise recline.js defaults to just 100.
48
- dataset.query({size: dataset.recordCount});
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 wanted_columns = import_csv_fields;
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(wanted_columns, normalised_csv_columns);
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
- _.each(data.records.models, function(record) {
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
- // Add a gap between each post to give the server
77
- // room to breathe
78
- setTimeout(function () {
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[import_csv_resource_name] = {};
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(wanted_columns, underscored_name)) {
87
- record_data[import_csv_resource_name][underscored_name] = attr[1];
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
- $.post(
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
- record_data,
94
- function(data) {
95
- succeeded = succeeded + 1;
96
- }).always(function() {
97
- loaded = loaded + 1;
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
- if (loaded == total) {
101
- progress.html("Done. Imported " + total + " records, " + succeeded + " succeeded.");
102
- if (redirect_path) {
103
- progress.html(progress.text() + " <a href='"+redirect_path +"'>Click to continue.</a>");
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
- }, 100 * i);
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,5 @@
1
+ <% @failures.each do |fail| %>
2
+ <div>
3
+ Row <%= (fail[:row_number].to_i + 2).to_s %>: <%= fail[:resource].errors.full_messages.to_sentence %>
4
+ </div>
5
+ <% end %>
@@ -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
- <h3>Import <%= active_admin_config.resource_name.pluralize.humanize %> from a CSV File<h3>
4
- <ul>
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 import_csv_fields = <%= @fields.to_json.html_safe %>;
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
- </script>
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
- @fields = options[:columns] ||= active_admin_config.resource_class.columns.map(&:name) - ["id", "updated_at", "created_at"]
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
@@ -1,3 +1,3 @@
1
1
  module ActiveAdminCsvImport
2
- VERSION = "1.1.0"
2
+ VERSION = "1.1.1"
3
3
  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 (e) {
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 (field) {
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
- // If the field was not quoted and we are trimming fields, trim it
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
- field = processField(field);
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 (s) {
160
+ return function(s) {
162
161
  return s.trim();
163
162
  };
164
163
  } else {
165
- return function (s) {
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.0
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-08-16 00:00:00.000000000 Z
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: &70194897983060 !ruby/object:Gem::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: *70194897983060
24
+ version_requirements: *70147043295120
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: railties
27
- requirement: &70194897981980 !ruby/object:Gem::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: *70194897981980
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: