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 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: