csv_row_model 0.4.1 → 1.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +162 -162
  3. data/csv_row_model.gemspec +2 -1
  4. data/lib/csv_row_model/concerns/{invalid_options.rb → check_options.rb} +4 -6
  5. data/lib/csv_row_model/export/attributes.rb +11 -20
  6. data/lib/csv_row_model/export/base.rb +3 -3
  7. data/lib/csv_row_model/export/cell.rb +24 -0
  8. data/lib/csv_row_model/export/dynamic_column_cell.rb +29 -0
  9. data/lib/csv_row_model/export/dynamic_columns.rb +11 -13
  10. data/lib/csv_row_model/export/file.rb +7 -7
  11. data/lib/csv_row_model/export/file_model.rb +2 -2
  12. data/lib/csv_row_model/export.rb +0 -3
  13. data/lib/csv_row_model/import/attributes.rb +18 -81
  14. data/lib/csv_row_model/import/base.rb +26 -69
  15. data/lib/csv_row_model/import/cell.rb +77 -0
  16. data/lib/csv_row_model/import/csv.rb +23 -60
  17. data/lib/csv_row_model/import/csv_string_model.rb +65 -0
  18. data/lib/csv_row_model/import/dynamic_column_cell.rb +37 -0
  19. data/lib/csv_row_model/import/dynamic_columns.rb +20 -37
  20. data/lib/csv_row_model/import/file/validations.rb +5 -1
  21. data/lib/csv_row_model/import/file.rb +8 -3
  22. data/lib/csv_row_model/import/file_model.rb +5 -4
  23. data/lib/csv_row_model/import/representation.rb +60 -0
  24. data/lib/csv_row_model/import/represents.rb +85 -0
  25. data/lib/csv_row_model/import.rb +4 -3
  26. data/lib/csv_row_model/model/base.rb +5 -15
  27. data/lib/csv_row_model/model/children.rb +2 -1
  28. data/lib/csv_row_model/model/columns.rb +19 -16
  29. data/lib/csv_row_model/model/comparison.rb +1 -1
  30. data/lib/csv_row_model/model/dynamic_column_cell.rb +44 -0
  31. data/lib/csv_row_model/model/dynamic_columns.rb +26 -11
  32. data/lib/csv_row_model/model.rb +4 -3
  33. data/lib/csv_row_model/version.rb +1 -1
  34. data/lib/csv_row_model.rb +3 -1
  35. metadata +29 -10
  36. data/lib/csv_row_model/concerns/inherited_class_var.rb +0 -121
  37. data/lib/csv_row_model/import/presenter.rb +0 -153
  38. data/lib/csv_row_model/model/csv_string_model.rb +0 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 44a0bf8c6a12a2a9a8c3cb74cf3563928313ac96
4
- data.tar.gz: a75edbc43ee598166facf68baf98555355e15660
3
+ metadata.gz: 620b2a84648e1723419994992518ecd9e77609d3
4
+ data.tar.gz: 5b331bc69fb075b9ca84bf53db592c0e6db15dde
5
5
  SHA512:
6
- metadata.gz: 9fa219f5da727166844f7d75519670fe0df6d98926caf0d72abafa8a84961af0513a87b69be00219671d5af2161edec57740e0128160d26952e7ef72db9cf69d
7
- data.tar.gz: 26a27334de6c098629a3d446eda5aece191bcd9080d732476e65e1534e79b7fb3e8501c22ae5f72ec3cfceef62e9600610b5fabec8a6405c4e43bb190fed7c17
6
+ metadata.gz: 5e10d1f2a7fd7115e632fadee715a852d9aa05bf5f1d69d04c79fc1503d598118bb75dfbb1707af4cf5afa6692c2338bd1d1e93125b39d1175445f34197dacad
7
+ data.tar.gz: 78994103e5eab1c176f5a2e9c7306073071a7da9adfc94344393e16cdd32fff6023c3f1e8e018b2f6ed8178f2720bbfc7149abba232a3b4414393ae6bc6bb8cf
data/README.md CHANGED
@@ -29,7 +29,7 @@ class ProjectExportRowModel < ProjectRowModel
29
29
  end
30
30
 
31
31
  export_file = CsvRowModel::Export::File.new(ProjectExportRowModel)
32
- export_file.generate { |csv| csv << project }
32
+ export_file.generate { |csv| csv << project } # `project` is the `source_model` in `ProjectExportRowModel`
33
33
  export_file.file # => <Tempfile>
34
34
  export_file.to_s # => export_file.file.read
35
35
  ```
@@ -86,10 +86,10 @@ To generate a header value, the following pseudocode is executed:
86
86
  ```ruby
87
87
  def header(column_name)
88
88
  # 1. Header Option
89
- header = options(column_name)[:header]
89
+ header = options_for(column_name)[:header]
90
90
 
91
91
  # 2. format_header
92
- header || format_header(column_name)
92
+ header || format_header(column_name, column_index, context)
93
93
  end
94
94
  ```
95
95
 
@@ -108,7 +108,7 @@ Override the `format_header` method to format column header names:
108
108
  class ProjectExportRowModel < ProjectRowModel
109
109
  include CsvRowModel::Export
110
110
  class << self
111
- def format_header(column_name)
111
+ def format_header(column_name, column_index, context)
112
112
  column_name.to_s.titleize
113
113
  end
114
114
  end
@@ -137,9 +137,8 @@ def original_attribute(column_name)
137
137
  end
138
138
  end
139
139
 
140
- def original_attributes; @original_attributes ||= { id: original_attribute(:id) } end
141
-
142
- def id; original_attribute[:id] end
140
+ def original_attributes; { id: original_attribute(:id) } end
141
+ def id; original_attribute(:id) end
143
142
  ```
144
143
 
145
144
  #### Format Cell
@@ -148,7 +147,7 @@ Override the `format_cell` method to clean/format every cell:
148
147
  class ProjectImportRowModel < ProjectRowModel
149
148
  include CsvRowModel::Import
150
149
  class << self
151
- def format_cell(cell, column_name, column_index, context={})
150
+ def format_cell(cell, column_name, column_index, context)
152
151
  cell = cell.strip
153
152
  cell.blank? ? nil : cell
154
153
  end
@@ -173,7 +172,7 @@ class ProjectImportRowModel
173
172
  end
174
173
  ```
175
174
 
176
- There are validators for different types: `Boolean`, `Date`, `DateTime`, `Float`, `Integer`. See [Validations](#validations) for more.
175
+ There are validators for different types: `Boolean`, `Date`, `DateTime`, `Float`, `Integer`. See [Type Format](#type-format) for more.
177
176
 
178
177
  #### Default
179
178
  Sets the default value of the cell:
@@ -193,157 +192,37 @@ row_model.name # => "John Doe"
193
192
  row_model.default_changes # => { id: ["", 1], name: ["", "John Doe"] }
194
193
  ```
195
194
 
196
- `DefaultChangeValidator` is provided to allows to add warnings when defaults are set. See [Validations](#default-changes) for more.
195
+ `DefaultChangeValidator` is provided to allows to add warnings when defaults are set. See [Default Changes](#default-changes) for more.
197
196
 
198
- ## Advanced Import
197
+ ### Validations
199
198
 
200
- ### Children
199
+ [`ActiveModel::Validations`](http://api.rubyonrails.org/classes/ActiveModel/Validations.html) and [`ActiveWarnings`](https://github.com/s12chung/active_warnings)
200
+ are included for errors and warnings.
201
201
 
202
- Child `RowModel` relationships can also be defined:
203
-
204
- ```ruby
205
- class UserImportRowModel
206
- include CsvRowModel::Model
207
- include CsvRowModel::Import
208
-
209
- column :id, type: Integer
210
- column :name
211
- column :email
212
-
213
- # uses ProjectImportRowModel#valid? to detect the child row
214
- has_many :projects, ProjectImportRowModel
215
- end
216
-
217
- import_file = CsvRowModel::Import::File.new(file_path, UserImportRowModel)
218
- row_model = import_file.next
219
- row_model.projects # => [<ProjectImportRowModel>, ...]
220
- ```
221
-
222
- ### Layers
223
- For complex `RowModel`s there are different layers you can work with:
224
- ```ruby
225
- import_file = CsvRowModel::Import::File.new(file_path, ProjectImportRowModel)
226
- row_model = import_file.next
227
-
228
- # the three layers:
229
- # 1. csv_string_model - represents the row BEFORE parsing (attributes are always strings)
230
- row_model.csv_string_model
231
-
232
- # 2. RowModel - represents the row AFTER parsing
233
- row_model
234
-
235
- # 3. Presenter - an abstraction of a row
236
- row_model.presenter
237
- ```
238
-
239
- #### CsvStringModel
240
- The `CsvStringModel` represents a row before parsing to add parsing validations.
202
+ There are layers to validations.
241
203
 
242
204
  ```ruby
243
205
  class ProjectImportRowModel
244
206
  include CsvRowModel::Model
245
207
  include CsvRowModel::Import
246
-
247
- # Note the type definition here for parsing
248
- column :id, type: Integer
249
-
250
- # this is applied to the parsed CSV on the model
251
- validates :id, numericality: { greater_than: 0 }
252
-
253
- csv_string_model do
254
- # define your csv_string_model here
255
-
256
- # this is applied BEFORE the parsed CSV on csv_string_model
257
- validates :id, presense: true
258
-
259
- def random_method; "Hihi" end
208
+
209
+ # Errors - by default, an Error will make the row skip
210
+ validates :id, numericality: { greater_than: 0 } # ActiveModel::Validations
211
+
212
+ # Warnings - a message you want the user to see, but will not make the row skip
213
+ warnings do # ActiveWarnings, see: https://github.com/s12chung/active_warnings
214
+ validates :some_custom_string, presence: true
260
215
  end
261
- end
262
-
263
- # Applied to the String
264
- ProjectImportRowModel.new([""])
265
- csv_string_model = row_model.csv_string_model
266
- csv_string_model.random_method => "Hihi"
267
- csv_string_model.valid? => false
268
- csv_string_model.errors.full_messages # => ["Id can't be blank'"]
269
-
270
- # Errors are propagated for simplicity
271
- row_model.valid? # => false
272
- row_model.errors.full_messages # => ["Id can't be blank'"]
273
-
274
- # Applied to the parsed Integer
275
- row_model = ProjectRowModel.new(["-1"])
276
- row_model.valid? # => false
277
- row_model.errors.full_messages # => ["Id must be greater than 0"]
278
- ```
279
-
280
- Note that `CsvStringModel` validations are calculated after [Format Cell](#format-cell).
281
-
282
- #### Presenter
283
- For complex rows, you can wrap your `RowModel` with a presenter:
284
-
285
- ```ruby
286
- class ProjectImportRowModel < ProjectRowModel
287
- include CsvRowModel::Import
288
-
289
- presenter do
290
- # define your presenter here
291
-
292
- # this is shorthand for the psuedo_code:
293
- # def project
294
- # return if row_model.id.blank? || row_model.name.blank?
295
- #
296
- # # turn off memoziation with `memoize: false` option
297
- # @project ||= __the_code_inside_the_block__
298
- # end
299
- #
300
- # and the psuedo_code:
301
- # def valid?
302
- # super # calls ActiveModel::Errors code
303
- # errors.delete(:project) if row_model.id.invalid? || row_model.name.invalid?
304
- # errors.empty?
305
- # end
306
- attribute :project, dependencies: [:id, :name] do
307
- project = Project.where(id: row_model.id).first
308
-
309
- # project not found, invalid.
310
- return unless project
311
-
312
- project.name = row_model.name
313
- project
314
- end
216
+
217
+ # This is for validation of the strings before parsing. See: https://github.com/FinalCAD/csv_row_model#csvstringmodel
218
+ csv_string_model do
219
+ validates :id, presence: true
220
+ # can do warnings too
315
221
  end
316
222
  end
317
-
318
- # Importing is the same
319
- import_file = CsvRowModel::Import::File.new(file_path, ProjectImportRowModel)
320
- row_model = import_file.next
321
- presenter = row_model.presenter
322
-
323
- presenter.row_model # gets the row model underneath
324
- presenter.project.name == presenter.row_model.name # => "Some Project Name"
325
223
  ```
326
224
 
327
- The presenters are designed for another layer of validation---such as with the database.
328
-
329
- Also, the `attribute` defines a dynamic `#project` method that:
330
-
331
- 1. Memoizes by default, turn off with `memoize: false` option
332
- 2. All errors of `row_model` are propagated to the presenter when calling `presenter.valid?`
333
- 3. Handles dependencies:
334
- - When any of the dependencies are `blank?`, the attribute block is not called and the attribute returns `nil`.
335
- - When any of the dependencies are `invalid?`, `presenter.errors` for dependencies are cleaned. For the example above, if `row_model.id/name` are `invalid?`, then
336
- the `:project` key is removed from the errors, so: `presenter.errors.keys # => [:id, :name]`
337
-
338
- ## Import Validations
339
-
340
- Use [`ActiveModel::Validations`](http://api.rubyonrails.org/classes/ActiveModel/Validations.html) the `RowModel`'s [Layers](#layers).
341
- Please read [Layers](#layers) for more information.
342
-
343
- Included is [`ActiveWarnings`](https://github.com/s12chung/active_warnings) on `Model` and `Presenter` for warnings.
344
-
345
-
346
- ### Type Format
225
+ #### Type Format
347
226
  Notice that there are validators given for different types: `Boolean`, `Date`, `DateTime`, `Float`, `Integer`:
348
227
 
349
228
  ```ruby
@@ -364,8 +243,8 @@ row_model.valid? # => false
364
243
  row_model.errors.full_messages # => ["Id is not a Integer format"]
365
244
  ```
366
245
 
367
- ### Default Changes
368
- [Default Changes](#default) are tracked within [`ActiveWarnings`](https://github.com/s12chung/active_warnings).
246
+ #### Default Changes
247
+ A custom validator for [Default Changes](#default).
369
248
 
370
249
  ```ruby
371
250
  class ProjectImportRowModel
@@ -373,17 +252,13 @@ class ProjectImportRowModel
373
252
  include CsvRowModel::Input
374
253
 
375
254
  column :id, default: 1
376
-
377
- warnings do
378
- validates :id, default_change: true
379
- end
255
+ validates :id, default_change: true
380
256
  end
381
257
 
382
258
  row_model = ProjectImportRowModel.new([""])
383
259
 
384
- row_model.unsafe? # => true
385
- row_model.has_warnings? # => true, same as `#unsafe?`
386
- row_model.warnings.full_messages # => ["Id changed by default"]
260
+ row_model.valid? # => false
261
+ row_model.errors.full_messages # => ["Id changed by default"]
387
262
  row_model.default_changes # => { id: ["", 1] }
388
263
  ```
389
264
 
@@ -396,13 +271,13 @@ abort logic:
396
271
  class ProjectImportRowModel
397
272
  # always skip
398
273
  def skip?
399
- true # original implementation: !valid? || presenter.skip?
274
+ true # original implementation: !valid?
400
275
  end
401
276
  end
402
277
 
403
- CsvRowModel::Import::File.new(file_path, ProjectImportRowModel).each do |project_import_model|
404
- # never yields here
405
- end
278
+ import_file = CsvRowModel::Import::File.new(file_path, ProjectImportRowModel)
279
+ import_file.each { |project_import_model| puts "does not yield here" }
280
+ import_file.next # does not skip or abort
406
281
  ```
407
282
 
408
283
  ### Import Callbacks
@@ -432,6 +307,128 @@ class ImportFile < CsvRowModel::Import::File
432
307
  end
433
308
  ```
434
309
 
310
+ ## Advanced Import
311
+
312
+ ### CsvStringModel
313
+ The `CsvStringModel` represents a row BEFORE parsing to add validations.
314
+
315
+ ```ruby
316
+ class ProjectImportRowModel
317
+ include CsvRowModel::Model
318
+ include CsvRowModel::Import
319
+
320
+ # Note the type definition here for parsing
321
+ column :id, type: Integer
322
+
323
+ # this is applied to the parsed CSV on the model
324
+ validates :id, numericality: { greater_than: 0 }
325
+
326
+ csv_string_model do
327
+ # define your csv_string_model here
328
+
329
+ # this is applied BEFORE the parsed CSV on csv_string_model
330
+ validates :id, presence: true
331
+
332
+ def random_method; "Hihi" end
333
+ end
334
+ end
335
+
336
+ # Applied to the String
337
+ ProjectImportRowModel.new([""])
338
+ csv_string_model = row_model.csv_string_model
339
+ csv_string_model.random_method => "Hihi"
340
+ csv_string_model.valid? => false
341
+ csv_string_model.errors.full_messages # => ["Id can't be blank'"]
342
+
343
+ # Errors are propagated for simplicity
344
+ row_model.valid? # => false
345
+ row_model.errors.full_messages # => ["Id can't be blank'"]
346
+
347
+ # Applied to the parsed Integer
348
+ row_model = ProjectRowModel.new(["-1"])
349
+ row_model.valid? # => false
350
+ row_model.errors.full_messages # => ["Id must be greater than 0"]
351
+ ```
352
+
353
+ Note that `CsvStringModel` validations are calculated after [Format Cell](#format-cell).
354
+
355
+ ### Represents
356
+ A CSV is often a representation of database model(s), much like how JSON parameters represents models in requests.
357
+ However, CSVs schemas are **flat** and **static** and JSON parameters are **tree structured** and **dynamic** (but often static).
358
+ Because CSVs are flat, `RowModel`s are also flat, but they can represent various models. The `represents` interface attempts to simplify this for importing.
359
+
360
+ ```ruby
361
+ class ProjectImportRowModel < ProjectRowModel
362
+ include CsvRowModel::Import
363
+
364
+ # this is shorthand for the psuedo_code:
365
+ # def project
366
+ # return if id.blank? || name.blank?
367
+ #
368
+ # # turn off memoziation with `memoize: false` option
369
+ # @project ||= __the_code_inside_the_block__
370
+ # end
371
+ #
372
+ # and the psuedo_code:
373
+ # def valid?
374
+ # super # calls ActiveModel::Errors code
375
+ # errors.delete(:project) if id.invalid? || name.invalid?
376
+ # errors.empty?
377
+ # end
378
+ represents_one :project, dependencies: [:id, :name] do
379
+ project = Project.where(id: id).first
380
+
381
+ # project not found, invalid.
382
+ return unless project
383
+
384
+ project.name = name
385
+ project
386
+ end
387
+
388
+ # same as above, but: returns [] if name.blank?
389
+ represents_many :projects, dependencies: [:name] do
390
+ Project.where(name: name)
391
+ end
392
+ end
393
+
394
+ # Importing is the same
395
+ import_file = CsvRowModel::Import::File.new(file_path, ProjectImportRowModel)
396
+ row_model = import_file.next
397
+ row_model.project.name # => "Some Project Name"
398
+ ```
399
+
400
+ The `represents_one` method defines a dynamic `#project` method that:
401
+
402
+ 1. Memoizes by default, turn off with `memoize: false` option
403
+ 2. Handles dependencies:
404
+ - When any of the dependencies are `blank?`, the attribute block is not called and the representation returns `nil`.
405
+ - When any of the dependencies are `invalid?`, `row_model.errors` for dependencies are cleaned. For the example above, if `id/name` are `invalid?`, then
406
+ the `:project` key is removed from the errors, so: `row_model.errors.keys # => [:id, :name]` (applies to warnings as well)
407
+
408
+ `represents_many` is also available, except it returns `[]` when any of the dependencies are `blank?`.
409
+
410
+ ### Children
411
+
412
+ Child `RowModel` relationships can also be defined:
413
+
414
+ ```ruby
415
+ class UserImportRowModel
416
+ include CsvRowModel::Model
417
+ include CsvRowModel::Import
418
+
419
+ column :id, type: Integer
420
+ column :name
421
+ column :email
422
+
423
+ # uses ProjectImportRowModel#valid? to detect the child row
424
+ has_many :projects, ProjectImportRowModel
425
+ end
426
+
427
+ import_file = CsvRowModel::Import::File.new(file_path, UserImportRowModel)
428
+ row_model = import_file.next
429
+ row_model.projects # => [<ProjectImportRowModel>, ...]
430
+ ```
431
+
435
432
  ## Dynamic columns
436
433
  Dynamic columns are columns that can expand to many columns. Currently, we can only one dynamic column after all other standard columns.
437
434
  The following:
@@ -456,6 +453,9 @@ represents this table:
456
453
  | Mike | Jackson | Yes | Yes |
457
454
 
458
455
 
456
+ The `format_dynamic_column_header(header_model, column_name, dynamic_column_index, index_of_column, context)` can
457
+ be used to defined like `format_header`. Defined in both import and export due to headers being used for both.
458
+
459
459
  ### Export
460
460
  Dynamic column attributes are arrays, but each item in the array is defined via singular attribute method like
461
461
  normal columns:
@@ -494,7 +494,7 @@ class DynamicColumnImportModel < DynamicColumnModel
494
494
  # Clean/format every dynamic_column attribute array
495
495
  #
496
496
  # this is an override with the default implementation
497
- def format_dynamic_column_cells(cells, column_name)
497
+ def format_dynamic_column_cells(cells, column_name, column_index, context)
498
498
  cells
499
499
  end
500
500
  end
@@ -518,7 +518,7 @@ class FileRowModel
518
518
  row :string1
519
519
  row :string2, header: 'String 2'
520
520
 
521
- def self.format_header(column_name, context={})
521
+ def self.format_header(column_name, column_index, context)
522
522
  ":: - #{column_name} - ::"
523
523
  end
524
524
  end
@@ -19,5 +19,6 @@ Gem::Specification.new do |spec|
19
19
  spec.require_paths = ["lib"]
20
20
 
21
21
  spec.add_dependency "activemodel", "~> 4.2"
22
- spec.add_dependency "active_warnings", ">= 0.1.2"
22
+ spec.add_dependency "active_warnings", "~> 0.1.2"
23
+ spec.add_dependency "inherited_class_var", ">= 0.2.2"
23
24
  end
@@ -1,15 +1,13 @@
1
1
  module CsvRowModel
2
2
  module Concerns
3
- module InvalidOptions
3
+ module CheckOptions
4
4
  extend ActiveSupport::Concern
5
5
 
6
6
  class_methods do
7
- protected
8
- def check_and_merge_options(options, default_options)
9
- invalid_options = options.keys - default_options.keys
7
+ def check_options(options)
8
+ invalid_options = options.keys - self::VALID_OPTIONS
10
9
  raise ArgumentError.new("Invalid option(s): #{invalid_options}") if invalid_options.present?
11
-
12
- options.reverse_merge(default_options)
10
+ true
13
11
  end
14
12
  end
15
13
  end
@@ -1,3 +1,5 @@
1
+ require 'csv_row_model/export/cell'
2
+
1
3
  module CsvRowModel
2
4
  module Export
3
5
  module Attributes
@@ -7,35 +9,24 @@ module CsvRowModel
7
9
  self.column_names.each { |*args| define_attribute_method(*args) }
8
10
  end
9
11
 
10
- # @return [Hash] a map of `column_name => self.class.format_cell(public_send(column_name))`
12
+ def cell_objects
13
+ @cell_objects ||= array_to_block_hash(self.class.column_names) { |column_name| Cell.new(column_name, self) }
14
+ end
15
+
16
+ # @return [Hash] a map of `column_name => formatted_attributes`
11
17
  def formatted_attributes
12
- formatted_attributes_from_column_names self.class.column_names
18
+ array_to_block_hash(self.class.column_names) { |column_name| formatted_attribute(column_name) }
13
19
  end
14
20
 
15
21
  def formatted_attribute(column_name)
16
- return public_send(column_name) if self.class.is_dynamic_column?(column_name)
17
-
18
- self.class.format_cell(
19
- public_send(column_name),
20
- column_name,
21
- self.class.index(column_name),
22
- context
23
- )
22
+ cell_objects[column_name].try(:value)
24
23
  end
25
24
 
26
- protected
27
- def formatted_attributes_from_column_names(column_names)
28
- array_to_block_hash(column_names) { |column_name| formatted_attribute(column_name) }
25
+ def source_attribute(column_name)
26
+ cell_objects[column_name].try(:source_value)
29
27
  end
30
28
 
31
29
  class_methods do
32
- # Safe to override. Method applied to each cell by default
33
- #
34
- # @param cell [Object] the cell's value
35
- def format_cell(cell, column_name, column_index, context={})
36
- cell
37
- end
38
-
39
30
  protected
40
31
  # See {Model#column}
41
32
  def column(column_name, options={})
@@ -4,15 +4,15 @@ module CsvRowModel
4
4
  extend ActiveSupport::Concern
5
5
 
6
6
  included do
7
- attr_reader :source_model, :context
7
+ attr_reader :source_model
8
8
  validates :source_model, presence: true
9
9
  end
10
10
 
11
11
  # @param [Model] source_model object to export to CSV
12
12
  # @param [Hash] context
13
- def initialize(source_model, context={})
13
+ def initialize(source_model=nil, context={})
14
14
  @source_model = source_model
15
- @context = OpenStruct.new(context)
15
+ super(context: context)
16
16
  end
17
17
 
18
18
  def to_rows
@@ -0,0 +1,24 @@
1
+ module CsvRowModel
2
+ module Export
3
+ class Cell
4
+ attr_reader :column_name, :row_model
5
+
6
+ def initialize(column_name, row_model)
7
+ @column_name = column_name
8
+ @row_model = row_model
9
+ end
10
+
11
+ def value
12
+ formatted_value
13
+ end
14
+
15
+ def formatted_value
16
+ @formatted_value ||= row_model.class.format_cell(source_value, column_name, row_model.class.index(column_name), row_model.context)
17
+ end
18
+
19
+ def source_value
20
+ row_model.public_send(column_name)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,29 @@
1
+ module CsvRowModel
2
+ module Export
3
+ class DynamicColumnCell < CsvRowModel::Model::DynamicColumnCell
4
+ def unformatted_value
5
+ formatted_cells
6
+ end
7
+
8
+ def formatted_cells
9
+ cells.map.with_index.map do |cell, index|
10
+ row_model.class.format_cell(cell, column_name, dynamic_column_index + index, row_model.context)
11
+ end
12
+ end
13
+
14
+ def cells
15
+ header_models.map { |header_model| call_process_cell(header_model) }
16
+ end
17
+
18
+ def header_models
19
+ row_model.context.public_send(column_name)
20
+ end
21
+
22
+ class << self
23
+ def define_process_cell(row_model_class, column_name)
24
+ super { |header_model| header_model }
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -1,3 +1,5 @@
1
+ require 'csv_row_model/export/dynamic_column_cell'
2
+
1
3
  module CsvRowModel
2
4
  module Export
3
5
  module DynamicColumns
@@ -7,6 +9,12 @@ module CsvRowModel
7
9
  self.dynamic_column_names.each { |*args| define_dynamic_attribute_method(*args) }
8
10
  end
9
11
 
12
+ def cell_objects
13
+ @dynamic_column_cell_objects ||= super.merge(array_to_block_hash(self.class.dynamic_column_names) do |column_name|
14
+ DynamicColumnCell.new(column_name, self)
15
+ end)
16
+ end
17
+
10
18
  # @return [Array] an array of public_send(column_name) of the CSV model
11
19
  def to_row
12
20
  super.flatten
@@ -14,7 +22,7 @@ module CsvRowModel
14
22
 
15
23
  # See Model::Export#formatted_attributes
16
24
  def formatted_attributes
17
- super.merge(formatted_attributes_from_column_names(self.class.dynamic_column_names))
25
+ super.merge!(array_to_block_hash(self.class.dynamic_column_names) { |column_name| formatted_attribute(column_name) })
18
26
  end
19
27
 
20
28
  class_methods do
@@ -29,18 +37,8 @@ module CsvRowModel
29
37
  # Define default attribute method for a dynamic_column
30
38
  # @param column_name [Symbol] the cell's column_name
31
39
  def define_dynamic_attribute_method(column_name)
32
- define_method(column_name) do
33
- context.public_send(column_name).map do |header_model|
34
- self.class.format_cell(
35
- public_send(self.class.singular_dynamic_attribute_method_name(column_name), header_model),
36
- column_name,
37
- self.class.dynamic_index(column_name),
38
- context
39
- )
40
- end
41
- end
42
-
43
- define_method(singular_dynamic_attribute_method_name(column_name)) { |header_model| header_model }
40
+ define_method(column_name) { formatted_attribute(column_name) }
41
+ DynamicColumnCell.define_process_cell(self, column_name)
44
42
  end
45
43
  end
46
44
  end