datagrid 1.8.0 → 1.8.2
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +24 -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 +4 -8
- 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
|