datagrid 1.8.1 → 1.8.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -44,7 +44,6 @@ module Datagrid
44
44
  class_attribute :cached, default: false
45
45
  class_attribute :decorator, instance_writer: false
46
46
  end
47
- base.include InstanceMethods
48
47
  end
49
48
 
50
49
  module ClassMethods
@@ -161,6 +160,9 @@ module Datagrid
161
160
  # @!visibility private
162
161
  def filter_columns(columns_array, *names, data: false, html: false)
163
162
  names.compact!
163
+ if names.size >= 1 && names.all? {|n| n.is_a?(Datagrid::Columns::Column) && n.grid_class == self.class}
164
+ return names
165
+ end
164
166
  names.map!(&:to_sym)
165
167
  columns_array.select do |column|
166
168
  (!data || column.data?) &&
@@ -173,7 +175,7 @@ module Datagrid
173
175
  def define_column(columns, name, query = nil, **options, &block)
174
176
  check_scope_defined!("Scope should be defined before columns")
175
177
  block ||= lambda do |model|
176
- model.send(name)
178
+ model.public_send(name)
177
179
  end
178
180
  position = Datagrid::Utils.extract_position_from_options(columns, options)
179
181
  column = Datagrid::Columns::Column.new(
@@ -193,326 +195,328 @@ module Datagrid
193
195
 
194
196
  end
195
197
 
196
- module InstanceMethods
197
-
198
- # @!visibility private
199
- def assets
200
- append_column_preload(
201
- driver.append_column_queries(
202
- super, columns.select(&:query)
203
- )
198
+ # @!visibility private
199
+ def assets
200
+ append_column_preload(
201
+ driver.append_column_queries(
202
+ super, columns.select(&:query)
204
203
  )
205
- end
204
+ )
205
+ end
206
206
 
207
- # @param column_names [Array<String>] list of column names if you want to limit data only to specified columns
208
- # @return [Array<String>] human readable column names. See also "Localization" section
209
- def header(*column_names)
210
- data_columns(*column_names).map(&:header)
211
- end
207
+ # @param column_names [Array<String>] list of column names if you want to limit data only to specified columns
208
+ # @return [Array<String>] human readable column names. See also "Localization" section
209
+ def header(*column_names)
210
+ data_columns(*column_names).map(&:header)
211
+ end
212
212
 
213
- # @param asset [Object] asset from datagrid scope
214
- # @param column_names [Array<String>] list of column names if you want to limit data only to specified columns
215
- # @return [Array<Object>] column values for given asset
216
- def row_for(asset, *column_names)
217
- data_columns(*column_names).map do |column|
218
- data_value(column, asset)
219
- end
213
+ # @param asset [Object] asset from datagrid scope
214
+ # @param column_names [Array<String>] list of column names if you want to limit data only to specified columns
215
+ # @return [Array<Object>] column values for given asset
216
+ def row_for(asset, *column_names)
217
+ data_columns(*column_names).map do |column|
218
+ data_value(column, asset)
220
219
  end
220
+ end
221
221
 
222
- # @param asset [Object] asset from datagrid scope
223
- # @return [Hash] A mapping where keys are column names and values are column values for the given asset
224
- def hash_for(asset)
225
- result = {}
226
- self.data_columns.each do |column|
227
- result[column.name] = data_value(column, asset)
228
- end
229
- result
222
+ # @param asset [Object] asset from datagrid scope
223
+ # @return [Hash] A mapping where keys are column names and values are column values for the given asset
224
+ def hash_for(asset)
225
+ result = {}
226
+ self.data_columns.each do |column|
227
+ result[column.name] = data_value(column, asset)
230
228
  end
229
+ result
230
+ end
231
231
 
232
- # @param column_names [Array<String>] list of column names if you want to limit data only to specified columns
233
- # @return [Array<Array<Object>>] with data for each row in datagrid assets without header
234
- def rows(*column_names)
235
- map_with_batches do |asset|
236
- self.row_for(asset, *column_names)
237
- end
232
+ # @param column_names [Array<String>] list of column names if you want to limit data only to specified columns
233
+ # @return [Array<Array<Object>>] with data for each row in datagrid assets without header
234
+ def rows(*column_names)
235
+ map_with_batches do |asset|
236
+ self.row_for(asset, *column_names)
238
237
  end
238
+ end
239
239
 
240
- # @param column_names [Array<String>] list of column names if you want to limit data only to specified columns
241
- # @return [Array<Array<Object>>] data for each row in datagrid assets with header.
242
- def data(*column_names)
243
- self.rows(*column_names).unshift(self.header(*column_names))
244
- end
240
+ # @param column_names [Array<String>] list of column names if you want to limit data only to specified columns
241
+ # @return [Array<Array<Object>>] data for each row in datagrid assets with header.
242
+ def data(*column_names)
243
+ self.rows(*column_names).unshift(self.header(*column_names))
244
+ end
245
245
 
246
- # Return Array of Hashes where keys are column names and values are column values
247
- # for each row in filtered datagrid relation.
248
- #
249
- # @example
250
- # class MyGrid
251
- # scope { Model }
252
- # column(:id)
253
- # column(:name)
254
- # end
255
- #
256
- # Model.create!(name: "One")
257
- # Model.create!(name: "Two")
258
- #
259
- # MyGrid.new.data_hash # => [{name: "One"}, {name: "Two"}]
260
- def data_hash
261
- map_with_batches do |asset|
262
- hash_for(asset)
263
- end
246
+ # Return Array of Hashes where keys are column names and values are column values
247
+ # for each row in filtered datagrid relation.
248
+ #
249
+ # @example
250
+ # class MyGrid
251
+ # scope { Model }
252
+ # column(:id)
253
+ # column(:name)
254
+ # end
255
+ #
256
+ # Model.create!(name: "One")
257
+ # Model.create!(name: "Two")
258
+ #
259
+ # MyGrid.new.data_hash # => [{name: "One"}, {name: "Two"}]
260
+ def data_hash
261
+ map_with_batches do |asset|
262
+ hash_for(asset)
264
263
  end
264
+ end
265
265
 
266
- # @param column_names [Array<String>]
267
- # @param options [Hash] CSV generation options
268
- # @return [String] a CSV representation of the data in the grid
269
- #
270
- # @example
271
- # grid.to_csv
272
- # grid.to_csv(:id, :name)
273
- # grid.to_csv(col_sep: ';')
274
- def to_csv(*column_names, **options)
275
- require "csv"
276
- CSV.generate(
277
- headers: self.header(*column_names),
278
- write_headers: true,
279
- **options
280
- ) do |csv|
281
- each_with_batches do |asset|
282
- csv << row_for(asset, *column_names)
283
- end
266
+ # @param column_names [Array<String>]
267
+ # @param options [Hash] CSV generation options
268
+ # @return [String] a CSV representation of the data in the grid
269
+ #
270
+ # @example
271
+ # grid.to_csv
272
+ # grid.to_csv(:id, :name)
273
+ # grid.to_csv(col_sep: ';')
274
+ def to_csv(*column_names, **options)
275
+ require "csv"
276
+ CSV.generate(
277
+ headers: self.header(*column_names),
278
+ write_headers: true,
279
+ **options
280
+ ) do |csv|
281
+ each_with_batches do |asset|
282
+ csv << row_for(asset, *column_names)
284
283
  end
285
284
  end
285
+ end
286
286
 
287
287
 
288
- # @param column_names [Array<Symbol, String>]
289
- # @return [Array<Datagrid::Columns::Column>] all columns selected in grid instance
290
- # @example
291
- # MyGrid.new.columns # => all defined columns
292
- # grid = MyGrid.new(column_names: [:id, :name])
293
- # grid.columns # => id and name columns
294
- # grid.columns(:id, :category) # => id and category column
295
- def columns(*column_names, data: false, html: false)
296
- self.class.filter_columns(
297
- columns_array, *column_names, data: data, html: html
298
- ).select do |column|
299
- column.enabled?(self)
300
- end
288
+ # @param column_names [Array<Symbol, String>]
289
+ # @return [Array<Datagrid::Columns::Column>] all columns selected in grid instance
290
+ # @example
291
+ # MyGrid.new.columns # => all defined columns
292
+ # grid = MyGrid.new(column_names: [:id, :name])
293
+ # grid.columns # => id and name columns
294
+ # grid.columns(:id, :category) # => id and category column
295
+ def columns(*column_names, data: false, html: false)
296
+ self.class.filter_columns(
297
+ columns_array, *column_names, data: data, html: html
298
+ ).select do |column|
299
+ column.enabled?(self)
301
300
  end
301
+ end
302
302
 
303
- # @param column_names [Array<String, Symbol>] list of column names if you want to limit data only to specified columns
304
- # @return columns that can be represented in plain data(non-html) way
305
- def data_columns(*column_names, **options)
306
- self.columns(*column_names, **options, data: true)
307
- end
303
+ # @param column_names [Array<String, Symbol>] list of column names if you want to limit data only to specified columns
304
+ # @return columns that can be represented in plain data(non-html) way
305
+ def data_columns(*column_names, **options)
306
+ self.columns(*column_names, **options, data: true)
307
+ end
308
308
 
309
- # @param column_names [Array<String>] list of column names if you want to limit data only to specified columns
310
- # @return all columns that can be represented in HTML table
311
- def html_columns(*column_names, **options)
312
- self.columns(*column_names, **options, html: true)
313
- end
309
+ # @param column_names [Array<String>] list of column names if you want to limit data only to specified columns
310
+ # @return all columns that can be represented in HTML table
311
+ def html_columns(*column_names, **options)
312
+ self.columns(*column_names, **options, html: true)
313
+ end
314
314
 
315
- # Finds a column definition by name
316
- # @param name [String, Symbol] column name to be found
317
- # @return [Datagrid::Columns::Column, nil]
318
- def column_by_name(name)
319
- self.class.find_column_by_name(columns_array, name)
320
- end
315
+ # Finds a column definition by name
316
+ # @param name [String, Symbol] column name to be found
317
+ # @return [Datagrid::Columns::Column, nil]
318
+ def column_by_name(name)
319
+ self.class.find_column_by_name(columns_array, name)
320
+ end
321
321
 
322
- # Gives ability to have a different formatting for CSV and HTML column value.
323
- #
324
- # @example
325
- # column(:name) do |model|
326
- # format(model.name) do |value|
327
- # content_tag(:strong, value)
328
- # end
329
- # end
330
- #
331
- # column(:company) do |model|
332
- # format(model.company.name) do
333
- # render partial: "company_with_logo", locals: {company: model.company }
334
- # end
335
- # end
336
- # @return [Datagrid::Columns::Column::ResponseFormat] Format object
337
- def format(value, &block)
338
- if block_given?
339
- self.class.format(value, &block)
340
- else
341
- # don't override Object#format method
342
- super
343
- end
322
+ # Gives ability to have a different formatting for CSV and HTML column value.
323
+ #
324
+ # @example
325
+ # column(:name) do |model|
326
+ # format(model.name) do |value|
327
+ # content_tag(:strong, value)
328
+ # end
329
+ # end
330
+ #
331
+ # column(:company) do |model|
332
+ # format(model.company.name) do
333
+ # render partial: "company_with_logo", locals: {company: model.company }
334
+ # end
335
+ # end
336
+ # @return [Datagrid::Columns::Column::ResponseFormat] Format object
337
+ def format(value, &block)
338
+ if block_given?
339
+ self.class.format(value, &block)
340
+ else
341
+ # don't override Object#format method
342
+ super
344
343
  end
344
+ end
345
345
 
346
- # @return [Datagrid::Columns::DataRow] an object representing a grid row.
347
- # @example
348
- # class MyGrid
349
- # scope { User }
350
- # column(:id)
351
- # column(:name)
352
- # column(:number_of_purchases) do |user|
353
- # user.purchases.count
354
- # end
355
- # end
356
- #
357
- # row = MyGrid.new.data_row(User.last)
358
- # row.id # => user.id
359
- # row.number_of_purchases # => user.purchases.count
360
- def data_row(asset)
361
- ::Datagrid::Columns::DataRow.new(self, asset)
362
- end
346
+ # @return [Datagrid::Columns::DataRow] an object representing a grid row.
347
+ # @example
348
+ # class MyGrid
349
+ # scope { User }
350
+ # column(:id)
351
+ # column(:name)
352
+ # column(:number_of_purchases) do |user|
353
+ # user.purchases.count
354
+ # end
355
+ # end
356
+ #
357
+ # row = MyGrid.new.data_row(User.last)
358
+ # row.id # => user.id
359
+ # row.number_of_purchases # => user.purchases.count
360
+ def data_row(asset)
361
+ ::Datagrid::Columns::DataRow.new(self, asset)
362
+ end
363
363
 
364
- # Defines a column at instance level
365
- #
366
- # @see Datagrid::Columns::ClassMethods#column
367
- def column(name, query = nil, **options, &block)
368
- self.class.define_column(columns_array, name, query, **options, &block)
369
- end
364
+ # Defines a column at instance level
365
+ #
366
+ # @see Datagrid::Columns::ClassMethods#column
367
+ def column(name, query = nil, **options, &block)
368
+ self.class.define_column(columns_array, name, query, **options, &block)
369
+ end
370
370
 
371
- # @!visibility private
372
- def initialize(*)
373
- self.columns_array = self.class.columns_array.clone
374
- super
371
+ # @!visibility private
372
+ def initialize(*)
373
+ self.columns_array = self.class.columns_array.clone
374
+ super
375
+ end
376
+
377
+ # @return [Array<Datagrid::Columns::Column>] all columns that are possible to be displayed for the current grid object
378
+ #
379
+ # @example
380
+ # class MyGrid
381
+ # filter(:search) {|scope, value| scope.full_text_search(value)}
382
+ # column(:id)
383
+ # column(:name, mandatory: true)
384
+ # column(:search_match, if: proc {|grid| grid.search.present? }) do |model, grid|
385
+ # search_match_line(model.searchable_content, grid.search)
386
+ # end
387
+ # end
388
+ #
389
+ # grid = MyGrid.new
390
+ # grid.columns # => [ #<Column:name> ]
391
+ # grid.available_columns # => [ #<Column:id>, #<Column:name> ]
392
+ # grid.search = "keyword"
393
+ # grid.available_columns # => [ #<Column:id>, #<Column:name>, #<Column:search_match> ]
394
+ def available_columns
395
+ columns_array.select do |column|
396
+ column.enabled?(self)
375
397
  end
398
+ end
376
399
 
377
- # @return [Array<Datagrid::Columns::Column>] all columns that are possible to be displayed for the current grid object
378
- #
379
- # @example
380
- # class MyGrid
381
- # filter(:search) {|scope, value| scope.full_text_search(value)}
382
- # column(:id)
383
- # column(:name, mandatory: true)
384
- # column(:search_match, if: proc {|grid| grid.search.present? }) do |model, grid|
385
- # search_match_line(model.searchable_content, grid.search)
386
- # end
387
- # end
388
- #
389
- # grid = MyGrid.new
390
- # grid.columns # => [ #<Column:name> ]
391
- # grid.available_columns # => [ #<Column:id>, #<Column:name> ]
392
- # grid.search = "keyword"
393
- # grid.available_columns # => [ #<Column:id>, #<Column:name>, #<Column:search_match> ]
394
- def available_columns
395
- columns_array.select do |column|
396
- column.enabled?(self)
397
- end
400
+ # @return [Object] a cell data value for given column name and asset
401
+ def data_value(column_name, asset)
402
+ column = column_by_name(column_name)
403
+ cache(column, asset, :data_value) do
404
+ raise "no data value for #{column.name} column" unless column.data?
405
+ result = generic_value(column, asset)
406
+ result.is_a?(Datagrid::Columns::Column::ResponseFormat) ? result.call_data : result
398
407
  end
408
+ end
399
409
 
400
- # @return [Object] a cell data value for given column name and asset
401
- def data_value(column_name, asset)
402
- column = column_by_name(column_name)
403
- cache(column, asset, :data_value) do
404
- raise "no data value for #{column.name} column" unless column.data?
410
+ # @return [Object] a cell HTML value for given column name and asset and view context
411
+ def html_value(column_name, context, asset)
412
+ column = column_by_name(column_name)
413
+ cache(column, asset, :html_value) do
414
+ if column.html? && column.html_block
415
+ value_from_html_block(context, asset, column)
416
+ else
405
417
  result = generic_value(column, asset)
406
- result.is_a?(Datagrid::Columns::Column::ResponseFormat) ? result.call_data : result
418
+ result.is_a?(Datagrid::Columns::Column::ResponseFormat) ? result.call_html(context) : result
407
419
  end
408
420
  end
421
+ end
409
422
 
410
- # @return [Object] a cell HTML value for given column name and asset and view context
411
- def html_value(column_name, context, asset)
412
- column = column_by_name(column_name)
413
- cache(column, asset, :html_value) do
414
- if column.html? && column.html_block
415
- value_from_html_block(context, asset, column)
416
- else
417
- result = generic_value(column, asset)
418
- result.is_a?(Datagrid::Columns::Column::ResponseFormat) ? result.call_html(context) : result
419
- end
423
+ # @return [Object] a decorated version of given model if decorator is specified or the model otherwise.
424
+ def decorate(model)
425
+ self.class.decorate(model)
426
+ end
427
+
428
+ # @!visibility private
429
+ def generic_value(column, model)
430
+ cache(column, model, :generic_value) do
431
+ presenter = decorate(model)
432
+ unless column.enabled?(self)
433
+ raise Datagrid::ColumnUnavailableError, "Column #{column.name} disabled for #{inspect}"
420
434
  end
421
- end
422
435
 
423
- # @return [Object] a decorated version of given model if decorator is specified or the model otherwise.
424
- def decorate(model)
425
- self.class.decorate(model)
436
+ if column.data_block.arity >= 1
437
+ Datagrid::Utils.apply_args(presenter, self, data_row(model), &column.data_block)
438
+ else
439
+ presenter.instance_eval(&column.data_block)
440
+ end
426
441
  end
442
+ end
427
443
 
428
- # @!visibility private
429
- def generic_value(column, model)
430
- cache(column, model, :generic_value) do
431
- presenter = decorate(model)
432
- unless column.enabled?(self)
433
- raise Datagrid::ColumnUnavailableError, "Column #{column.name} disabled for #{inspect}"
434
- end
444
+ # @!visibility private
445
+ def reset
446
+ super
447
+ @cache = {}
448
+ end
435
449
 
436
- if column.data_block.arity >= 1
437
- Datagrid::Utils.apply_args(presenter, self, data_row(model), &column.data_block)
438
- else
439
- presenter.instance_eval(&column.data_block)
440
- end
441
- end
450
+ protected
442
451
 
452
+ def append_column_preload(relation)
453
+ columns.inject(relation) do |current, column|
454
+ column.append_preload(current)
443
455
  end
456
+ end
444
457
 
445
- protected
446
-
447
- def append_column_preload(relation)
448
- columns.inject(relation) do |current, column|
449
- column.append_preload(current)
450
- end
458
+ def cache(column, asset, type)
459
+ @cache ||= {}
460
+ unless cached?
461
+ @cache.clear
462
+ return yield
451
463
  end
452
-
453
- def cache(column, asset, type)
454
- @cache ||= {}
455
- unless cached?
456
- @cache.clear
457
- return yield
458
- end
459
- key = cache_key(asset)
460
- unless key
461
- raise(Datagrid::CacheKeyError, "Datagrid Cache key is #{key.inspect} for #{asset.inspect}.")
462
- end
463
- @cache[column.name] ||= {}
464
- @cache[column.name][key] ||= {}
465
- @cache[column.name][key][type] ||= yield
464
+ key = cache_key(asset)
465
+ unless key
466
+ raise(Datagrid::CacheKeyError, "Datagrid Cache key is #{key.inspect} for #{asset.inspect}.")
466
467
  end
468
+ @cache[column.name] ||= {}
469
+ @cache[column.name][key] ||= {}
470
+ @cache[column.name][key][type] ||= yield
471
+ end
467
472
 
468
- def cache_key(asset)
469
- if cached.respond_to?(:call)
470
- cached.call(asset)
471
- else
472
- driver.default_cache_key(asset)
473
- end
474
- rescue NotImplementedError
475
- raise Datagrid::ConfigurationError, "#{self} is setup to use cache. But there was appropriate cache key found for #{asset.inspect}. Please set cached option to block with asset as argument and cache key as returning value to resolve the issue."
473
+ def cache_key(asset)
474
+ if cached.respond_to?(:call)
475
+ cached.call(asset)
476
+ else
477
+ driver.default_cache_key(asset)
476
478
  end
479
+ rescue NotImplementedError
480
+ raise Datagrid::ConfigurationError, "#{self} is setup to use cache. But there was appropriate cache key found for #{asset.inspect}. Please set cached option to block with asset as argument and cache key as returning value to resolve the issue."
481
+ end
477
482
 
478
483
 
479
- def map_with_batches(&block)
480
- result = []
481
- each_with_batches do |asset|
482
- result << block.call(asset)
483
- end
484
- result
484
+ def map_with_batches(&block)
485
+ result = []
486
+ each_with_batches do |asset|
487
+ result << block.call(asset)
485
488
  end
489
+ result
490
+ end
486
491
 
487
- def each_with_batches(&block)
488
- if batch_size && batch_size > 0
489
- driver.batch_each(assets, batch_size, &block)
490
- else
491
- assets.each(&block)
492
- end
492
+ def each_with_batches(&block)
493
+ if batch_size && batch_size > 0
494
+ driver.batch_each(assets, batch_size, &block)
495
+ else
496
+ assets.each(&block)
493
497
  end
498
+ end
494
499
 
495
- def value_from_html_block(context, asset, column)
496
- args = []
497
- remaining_arity = column.html_block.arity
498
- remaining_arity = 1 if remaining_arity < 0
500
+ def value_from_html_block(context, asset, column)
501
+ args = []
502
+ remaining_arity = column.html_block.arity
503
+ remaining_arity = 1 if remaining_arity < 0
499
504
 
500
- asset = decorate(asset)
505
+ asset = decorate(asset)
501
506
 
502
- if column.data?
503
- args << data_value(column, asset)
504
- remaining_arity -= 1
505
- end
507
+ if column.data?
508
+ args << data_value(column, asset)
509
+ remaining_arity -= 1
510
+ end
506
511
 
507
- args << asset if remaining_arity > 0
508
- args << self if remaining_arity > 1
512
+ args << asset if remaining_arity > 0
513
+ args << self if remaining_arity > 1
509
514
 
510
- context.instance_exec(*args, &column.html_block)
511
- end
515
+ context.instance_exec(*args, &column.html_block)
512
516
  end
513
517
 
514
518
  # Object representing a single row of data when building a datagrid table
515
- # @see Datagrid::Columns::InstanceMethods#data_row
519
+ # @see Datagrid::Columns#data_row
516
520
  class DataRow < BasicObject
517
521
  def initialize(grid, model)
518
522
  @grid = grid