importable_attachments 0.0.13 → 0.0.14

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.
@@ -1,3 +1,4 @@
1
+ require 'importable_attachments/importers/importer'
1
2
  module ImportableAttachments::Base
2
3
  module ClassMethods
3
4
 
@@ -20,6 +21,7 @@ module ImportableAttachments::Base
20
21
  install_importable_attachment_assignment_protection
21
22
 
22
23
  include InstanceMethods
24
+ include ImportableAttachments::Importers::Importer
23
25
 
24
26
  # for assigning attachment to new record
25
27
  after_create :import_attachment
@@ -52,6 +54,9 @@ module ImportableAttachments::Base
52
54
  def install_importable_attachment_validations
53
55
  validates :attachment, :associated => true
54
56
  validate do |record|
57
+ if @invalid_extension
58
+ invalid_attachment_error "invalid extension: .#{@invalid_extension}"
59
+ end
55
60
  if @columns_not_found
56
61
  invalid_attachment_error "column(s) not found: #{@columns_not_found}"
57
62
  end
@@ -61,10 +66,6 @@ module ImportableAttachments::Base
61
66
  end
62
67
  end
63
68
 
64
- # These go in the class calling has_importable_attachment as they are
65
- # dependent on mime-type expectations
66
- #validates_with CsvValidator, :if => Proc.new {|model| model.attachment.present?}
67
- #validates_with ExcelValidator, :if => Proc.new {|model| model.attachment.present?}
68
69
  end
69
70
 
70
71
  def install_importable_attachment_assignment_protection
@@ -76,29 +77,9 @@ module ImportableAttachments::Base
76
77
 
77
78
  module InstanceMethods
78
79
  # : call-seq:
79
- # import_attachment
80
- #
81
- # imports an attachment of a given mime-type (data-stream to ruby),
82
- # calls import_rows with a ruby data-store
83
- #
84
- # NOTE: this is a stub
85
-
86
- def import_attachment
87
- raise RuntimeError, '[importable_attachments] .import_attachment not implemented'
88
- end
89
-
90
- # : call-seq:
91
- # import_rows params
92
- #
93
- # imports an attachment contents into :import_into association
80
+ # invalid_attachment_error msg
94
81
  #
95
- # NOTE: this is a stub
96
-
97
- def import_rows(*opts)
98
- return unless self.attachment
99
- logger.debug "[importable_attachments] .import_rows #{opts}"
100
- raise RuntimeError, '[importable_attachments] .import_rows not implemented'
101
- end
82
+ # adds errors to base record and attachment
102
83
 
103
84
  def invalid_attachment_error(msg)
104
85
  attachment.errors.add(:base, msg)
@@ -1,12 +1,17 @@
1
+ require 'roo'
2
+
1
3
  module ImportableAttachments
2
4
  module Importers
3
- module Csv
5
+ module Importer
4
6
  attr_accessor :validate_headers, :destructive_import, :validate_on_import
5
7
 
6
8
  # ImportInto suitable attributes translated from a ImportInto::RECORD_HEADERS
7
9
  # inversion, based on RECORD_HEADERS
8
10
  attr_accessor :converted_headers
9
11
 
12
+ # stores the parsed-file for later processing
13
+ attr_accessor :attachment_as_ruby
14
+
10
15
  def initialize(attributes = nil, options = {})
11
16
  bootstrap
12
17
  super(attributes, options)
@@ -37,16 +42,18 @@ module ImportableAttachments
37
42
 
38
43
  def attachment=(params)
39
44
  super params
40
- import_attachment if persisted? && attachment && attachment.valid?
45
+ import_attachment if persisted? && attachment.try(:valid?)
41
46
  end
42
47
 
43
- # :call-seq:
44
- # import_csv
48
+ # : call-seq:
49
+ # import_attachment
45
50
  #
46
- # imports a comma-separated value file
51
+ # imports an attachment of a given mime-type (data-stream to ruby),
52
+ # calls import_rows with a ruby data-store
47
53
 
48
- def import_csv
54
+ def import_attachment
49
55
  return unless attachment.present?
56
+ return unless read_spreadsheet
50
57
  return if validate_headers && !importable_class_headers_ok?
51
58
  transaction do
52
59
  send(association_symbol_for_rows).destroy_all if destructive_import
@@ -69,9 +76,9 @@ module ImportableAttachments
69
76
 
70
77
  # .dup else .import modifies converted_headers and spreadsheet
71
78
  if respond_to? :sanitize_data_callback
72
- headers, sheet = sanitize_data_callback(converted_headers, spreadsheet)
79
+ headers, sheet = sanitize_data_callback(@converted_headers, spreadsheet)
73
80
  else
74
- headers, sheet = converted_headers.dup, spreadsheet.dup
81
+ headers, sheet = @converted_headers.dup, spreadsheet.dup
75
82
  end
76
83
  results = @import_rows_to_class.import headers, sheet, importer_opts
77
84
  reload if persisted?
@@ -134,16 +141,36 @@ module ImportableAttachments
134
141
  # :call-seq:
135
142
  # read_spreadsheet
136
143
  #
137
- # the "raw" file as processed by CSV
144
+ # sets @attachment_as_ruby to the raw file as processed by roo if the file can be read
138
145
 
139
146
  def read_spreadsheet
140
- csv_klass = (defined? FasterCSV) ? FasterCSV : CSV
141
- stream = attachment.io_stream
142
- if stream.exists?
143
- csv_klass.read stream.path
147
+ if !%w(xls xlsx ods xml csv).member?(stream_extension) # required for roo - it checks file extension
148
+ @invalid_extension = stream_extension
144
149
  else
145
- csv_klass.read stream.queued_for_write[:original].path
150
+ @invalid_extension = nil
151
+ spreadsheet = Roo::Spreadsheet.open stream_path
152
+ @attachment_as_ruby = spreadsheet.parse
146
153
  end
154
+ @attachment_as_ruby
155
+ end
156
+
157
+ # :call-seq:
158
+ # stream_path
159
+ #
160
+ # yields path for a readable file, saved or not
161
+
162
+ def stream_path
163
+ @stream = attachment.io_stream
164
+ @stream.exists? ? @stream.path : @stream.queued_for_write[:original].path
165
+ end
166
+
167
+ # :call-seq:
168
+ # stream_extension
169
+ #
170
+ # yields extension for file
171
+
172
+ def stream_extension
173
+ stream_path.split('.').last
147
174
  end
148
175
 
149
176
  # :call-seq:
@@ -152,7 +179,7 @@ module ImportableAttachments
152
179
  # the rows of the file after the first row (headers)
153
180
 
154
181
  def spreadsheet
155
- read_spreadsheet[1..-1]
182
+ @attachment_as_ruby[1..-1]
156
183
  end
157
184
 
158
185
  # :call-seq:
@@ -161,7 +188,7 @@ module ImportableAttachments
161
188
  # headers for the spreadsheet
162
189
 
163
190
  def headers
164
- read_spreadsheet.first
191
+ @attachment_as_ruby.first
165
192
  end
166
193
 
167
194
  # :call-seq:
@@ -195,11 +222,14 @@ module ImportableAttachments
195
222
  # translates English date-ish and-or time-ish language into DateTime instances
196
223
 
197
224
  def convert_datetimes_intelligently!
198
- dt_attrs = converted_headers.select { |obj| obj.match(/_(?:dt?|at|on)\z/) }
199
- dt_idxs = dt_attrs.map { |obj| converted_headers.find_index(obj) }
225
+ dt_attrs = @converted_headers.select { |obj| obj.match(/_(?:dt?|at|on)\z/) }
226
+ dt_idxs = dt_attrs.map { |obj| @converted_headers.find_index(obj) }
200
227
 
201
228
  spreadsheet.map! { |row|
202
- dt_idxs.each { |idx| row[idx] = row[idx].try(:to_datetime) || row[idx] }
229
+ dt_idxs.each { |idx|
230
+ to_convert = row[idx]
231
+ row[idx] = to_convert.try(:to_datetime) || to_convert
232
+ }
203
233
  row }
204
234
  end
205
235
 
@@ -1,3 +1,3 @@
1
1
  module ImportableAttachments
2
- VERSION = '0.0.13'
2
+ VERSION = '0.0.14'
3
3
  end
Binary file
@@ -1,5 +1,3 @@
1
- require 'importable_attachments/importers'
2
-
3
1
  class Library < ActiveRecord::Base
4
2
  include SmarterDates if ::Configuration.for('smarter_dates').enabled
5
3
 
@@ -13,8 +11,6 @@ class Library < ActiveRecord::Base
13
11
  extend ImportableAttachments::Base::ClassMethods
14
12
  has_importable_attachment spreadsheet_columns: RECORD_HEADERS,
15
13
  import_into: :books
16
- include ImportableAttachments::Importers::Csv
17
- validates_with ImportableAttachments::CsvValidator, if: Proc.new { |model| model.attachment.present? && model.attachment.persisted? }
18
14
  end
19
15
 
20
16
  # --------------------------------------------------------------------------
@@ -35,16 +31,6 @@ class Library < ActiveRecord::Base
35
31
  # --------------------------------------------------------------------------
36
32
  # define: behaviors
37
33
 
38
- # : call-seq:
39
- # import_attachment
40
- #
41
- # imports an attachment of a given mime-type (data-stream to ruby),
42
- # calls import_rows with a ruby data-store
43
-
44
- def import_attachment
45
- import_csv
46
- end
47
-
48
34
  protected
49
35
 
50
36
  def sanitize_data_callback(headers, sheet) # :nodoc:
@@ -151,5 +151,19 @@ describe Library do
151
151
  end
152
152
  end
153
153
 
154
+ context 'importing excel file' do
155
+ before :each do
156
+ @attachment = Attachment.new io_stream: File.new(spec_file('books.xls'), 'rb')
157
+ end
158
+
159
+ subject { Library.create(name: 'XYZ Library', address: '123 Main St.') }
160
+
161
+ it 'should import a spreadsheet if it is in the right format' do
162
+ subject.books.should be_empty
163
+ lambda {
164
+ subject.attachment = @attachment
165
+ }.should change(subject.books, :count).by(5)
166
+ end
167
+ end
154
168
  end
155
169
 
metadata CHANGED
@@ -2,14 +2,14 @@
2
2
  name: importable_attachments
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 0.0.13
5
+ version: 0.0.14
6
6
  platform: ruby
7
7
  authors:
8
8
  - Paul Belt
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-05-30 00:00:00.000000000 Z
12
+ date: 2013-06-04 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: haml-rails
@@ -524,9 +524,6 @@ files:
524
524
  - app/models/importable_attachments/attachment.rb
525
525
  - app/models/importable_attachments/version.rb
526
526
  - app/validators/existing_class_validator.rb
527
- - app/validators/importable_attachments/csv_validator.rb
528
- - app/validators/importable_attachments/excel.rb
529
- - app/validators/importable_attachments/excel_validator.rb
530
527
  - app/views/importable_attachments/attachments/_attachment.html.haml
531
528
  - app/views/importable_attachments/attachments/_form.html.haml
532
529
  - app/views/importable_attachments/attachments/_nested_form.html.haml
@@ -566,14 +563,14 @@ files:
566
563
  - lib/importable_attachments/blueprints.rb
567
564
  - lib/importable_attachments/engine.rb
568
565
  - lib/importable_attachments/importers.rb
569
- - lib/importable_attachments/importers/csv.rb
570
- - lib/importable_attachments/importers/excel.rb
566
+ - lib/importable_attachments/importers/importer.rb
571
567
  - lib/importable_attachments/version.rb
572
568
  - lib/paperclip_processors/save_upload.rb
573
569
  - lib/tasks/importable_attachments_tasks.rake
574
570
  - script/rails
575
571
  - spec/attachments/books.csv
576
572
  - spec/attachments/books.txt
573
+ - spec/attachments/books.xls
577
574
  - spec/attachments/books2.csv
578
575
  - spec/attachments/empty.csv
579
576
  - spec/attachments/failed_instances.csv
@@ -669,6 +666,7 @@ summary: upload, save-to-disk, attach-to-model_instance, importing
669
666
  test_files:
670
667
  - spec/attachments/books.csv
671
668
  - spec/attachments/books.txt
669
+ - spec/attachments/books.xls
672
670
  - spec/attachments/books2.csv
673
671
  - spec/attachments/empty.csv
674
672
  - spec/attachments/failed_instances.csv
@@ -1,36 +0,0 @@
1
- require 'csv'
2
-
3
- # validate attachment is a CSV file
4
- module ImportableAttachments # :nodoc:
5
- class CsvValidator < ActiveModel::Validator
6
-
7
- # :call-seq:
8
- # validate :record
9
- #
10
- # ensures that the record's attachment file name has a .xls extension
11
-
12
- def validate(record)
13
- extension = record.attachment.io_stream_file_name.split('.').last
14
- if extension.downcase != 'csv'
15
- record.errors.add :attachment, 'invalid attachment'
16
- record.attachment.errors.add :base, 'File must be a CSV (.csv) file'
17
- end
18
-
19
- if defined? FasterCSV
20
- begin
21
- FasterCSV.read record.attachment.io_stream
22
- rescue FasterCSV::MalformedCSVError => err
23
- record.errors.add :attachment, 'invalid attachment'
24
- record.attachment.errors.add :base, err.messages.join(', ')
25
- end
26
- else
27
- begin
28
- CSV.read record.attachment.io_stream.path
29
- rescue CSV::MalformedCSVError => err
30
- record.errors.add :attachment, 'invalid attachment'
31
- record.attachment.errors.add :base, err.messages.join(', ')
32
- end
33
- end
34
- end
35
- end
36
- end
@@ -1,18 +0,0 @@
1
- module ImportableAttachments # :nodoc:
2
- # validate attachment is an excel file
3
- class ExcelValidator < ActiveModel::Validator
4
-
5
- # :call-seq:
6
- # validate :record
7
- #
8
- # ensures that the record's attachment file name has a .xls extension
9
-
10
- def validate(record)
11
- extension = record.attachment.io_stream_file_name.split('.').last
12
- if extension != 'xls'
13
- record.errors[:attachment] << 'File must be an Excel (.xls) file'
14
- end
15
- end
16
-
17
- end
18
- end
@@ -1,18 +0,0 @@
1
- module ImportableAttachments # :nodoc:
2
- # validate attachment is an excel file
3
- class ExcelValidator < ActiveModel::Validator
4
-
5
- # :call-seq:
6
- # validate :record
7
- #
8
- # ensures that the record's attachment file name has a .xls extension
9
-
10
- def validate(record)
11
- extension = record.attachment.io_stream_file_name.split('.').last
12
- if extension != 'xls'
13
- record.errors[:attachment] << 'File must be an Excel (.xls) file'
14
- end
15
- end
16
-
17
- end
18
- end
@@ -1,37 +0,0 @@
1
- module ImportableAttachments
2
- module Importers
3
- module Excel
4
-
5
- require 'iconv'
6
-
7
- # :call-seq:
8
- # import_csv
9
- #
10
- # imports an Excel (tm) file
11
-
12
- def import_excel
13
- column_names = self.class.spreadsheet_columns
14
- assoc = self.class.import_into
15
- import_method = self.class.import_method
16
- return unless attachment.present?
17
-
18
- stream = attachment.io_stream
19
- stream_path = if stream.exists?
20
- stream.path
21
- else
22
- stream.queued_for_write[:original].path
23
- end
24
- spreadsheet = Roo::Excel.new stream_path
25
-
26
- spreadsheet.default_sheet = spreadsheet.sheets.first
27
- headers = (1..column_names.length).map { |n| spreadsheet.cell(1, n).try(:downcase) }
28
- return unless headers == column_names.map(&:downcase)
29
- self.send(assoc).destroy_all
30
- 2.upto(spreadsheet.last_row) do |line|
31
- self.send(import_method, *(1..column_names.length).map { |n| spreadsheet.cell(line, n) })
32
- end
33
- end
34
-
35
- end
36
- end
37
- end