datagrid 1.8.1 → 1.8.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -6
- data/Readme.markdown +4 -4
- data/app/assets/stylesheets/datagrid.sass +1 -1
- data/app/views/datagrid/_form.html.erb +0 -1
- data/datagrid.gemspec +3 -3
- data/lib/datagrid/column_names_attribute.rb +3 -1
- data/lib/datagrid/columns.rb +268 -264
- data/lib/datagrid/core.rb +132 -133
- data/lib/datagrid/drivers/abstract_driver.rb +1 -2
- data/lib/datagrid/drivers/array.rb +10 -9
- data/lib/datagrid/drivers/mongoid.rb +1 -1
- data/lib/datagrid/filters/base_filter.rb +3 -3
- data/lib/datagrid/filters/date_filter.rb +1 -1
- data/lib/datagrid/filters/extended_boolean_filter.rb +6 -3
- data/lib/datagrid/filters.rb +67 -72
- data/lib/datagrid/form_builder.rb +2 -2
- data/lib/datagrid/ordering.rb +71 -74
- data/lib/datagrid/rspec.rb +2 -2
- data/lib/datagrid/scaffold.rb +3 -3
- data/lib/datagrid/utils.rb +7 -10
- data/lib/datagrid/version.rb +1 -1
- data/templates/base.rb.erb +27 -3
- metadata +8 -8
data/lib/datagrid/columns.rb
CHANGED
@@ -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.
|
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
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
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
|
-
|
204
|
+
)
|
205
|
+
end
|
206
206
|
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
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
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
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
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
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
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
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
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
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
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
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
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
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
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
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
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
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
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
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
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
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
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
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
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
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
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
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
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
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
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
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
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
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.
|
418
|
+
result.is_a?(Datagrid::Columns::Column::ResponseFormat) ? result.call_html(context) : result
|
407
419
|
end
|
408
420
|
end
|
421
|
+
end
|
409
422
|
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
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
|
-
|
424
|
-
|
425
|
-
|
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
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
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
|
-
|
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
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
end
|
458
|
+
def cache(column, asset, type)
|
459
|
+
@cache ||= {}
|
460
|
+
unless cached?
|
461
|
+
@cache.clear
|
462
|
+
return yield
|
451
463
|
end
|
452
|
-
|
453
|
-
|
454
|
-
|
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
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
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
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
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
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
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
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
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
|
-
|
505
|
+
asset = decorate(asset)
|
501
506
|
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
507
|
+
if column.data?
|
508
|
+
args << data_value(column, asset)
|
509
|
+
remaining_arity -= 1
|
510
|
+
end
|
506
511
|
|
507
|
-
|
508
|
-
|
512
|
+
args << asset if remaining_arity > 0
|
513
|
+
args << self if remaining_arity > 1
|
509
514
|
|
510
|
-
|
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
|
519
|
+
# @see Datagrid::Columns#data_row
|
516
520
|
class DataRow < BasicObject
|
517
521
|
def initialize(grid, model)
|
518
522
|
@grid = grid
|