csv_row_model 0.4.1 → 1.0.0.beta1

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