datagrid 1.8.1 → 2.0.0.pre.alpha

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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -7
  3. data/{Readme.markdown → README.md} +46 -29
  4. data/app/assets/stylesheets/datagrid.css +145 -0
  5. data/app/views/datagrid/_enum_checkboxes.html.erb +5 -3
  6. data/app/views/datagrid/_form.html.erb +4 -5
  7. data/app/views/datagrid/_head.html.erb +26 -3
  8. data/app/views/datagrid/_range_filter.html.erb +5 -3
  9. data/app/views/datagrid/_row.html.erb +12 -1
  10. data/app/views/datagrid/_table.html.erb +4 -4
  11. data/datagrid.gemspec +8 -8
  12. data/lib/datagrid/active_model.rb +9 -17
  13. data/lib/datagrid/base.rb +39 -0
  14. data/lib/datagrid/column_names_attribute.rb +12 -12
  15. data/lib/datagrid/columns/column.rb +155 -133
  16. data/lib/datagrid/columns.rb +495 -282
  17. data/lib/datagrid/configuration.rb +23 -10
  18. data/lib/datagrid/core.rb +184 -150
  19. data/lib/datagrid/deprecated_object.rb +20 -0
  20. data/lib/datagrid/drivers/abstract_driver.rb +13 -25
  21. data/lib/datagrid/drivers/active_record.rb +24 -26
  22. data/lib/datagrid/drivers/array.rb +26 -17
  23. data/lib/datagrid/drivers/mongo_mapper.rb +15 -14
  24. data/lib/datagrid/drivers/mongoid.rb +16 -18
  25. data/lib/datagrid/drivers/sequel.rb +14 -19
  26. data/lib/datagrid/drivers.rb +2 -1
  27. data/lib/datagrid/engine.rb +11 -3
  28. data/lib/datagrid/filters/base_filter.rb +166 -142
  29. data/lib/datagrid/filters/boolean_filter.rb +19 -5
  30. data/lib/datagrid/filters/date_filter.rb +33 -35
  31. data/lib/datagrid/filters/date_time_filter.rb +24 -16
  32. data/lib/datagrid/filters/default_filter.rb +9 -3
  33. data/lib/datagrid/filters/dynamic_filter.rb +151 -105
  34. data/lib/datagrid/filters/enum_filter.rb +43 -19
  35. data/lib/datagrid/filters/extended_boolean_filter.rb +39 -27
  36. data/lib/datagrid/filters/float_filter.rb +16 -5
  37. data/lib/datagrid/filters/integer_filter.rb +21 -10
  38. data/lib/datagrid/filters/ranged_filter.rb +66 -45
  39. data/lib/datagrid/filters/select_options.rb +58 -49
  40. data/lib/datagrid/filters/string_filter.rb +9 -4
  41. data/lib/datagrid/filters.rb +234 -106
  42. data/lib/datagrid/form_builder.rb +116 -128
  43. data/lib/datagrid/generators/scaffold.rb +185 -0
  44. data/lib/datagrid/generators/views.rb +20 -0
  45. data/lib/datagrid/helper.rb +397 -22
  46. data/lib/datagrid/ordering.rb +81 -87
  47. data/lib/datagrid/rspec.rb +8 -12
  48. data/lib/datagrid/utils.rb +42 -38
  49. data/lib/datagrid/version.rb +3 -1
  50. data/lib/datagrid.rb +18 -28
  51. data/templates/base.rb.erb +33 -7
  52. data/templates/grid.rb.erb +1 -1
  53. metadata +18 -19
  54. data/app/assets/stylesheets/datagrid.sass +0 -134
  55. data/lib/datagrid/filters/composite_filters.rb +0 -49
  56. data/lib/datagrid/renderer.rb +0 -157
  57. data/lib/datagrid/scaffold.rb +0 -129
  58. data/lib/tasks/datagrid_tasks.rake +0 -15
  59. data/templates/controller.rb.erb +0 -6
  60. data/templates/index.html.erb +0 -5
@@ -1,13 +1,208 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "datagrid/utils"
2
4
  require "active_support/core_ext/class/attribute"
3
5
 
4
6
  module Datagrid
5
-
7
+ # Defines a column to be used for displaying data in a Datagrid.
8
+ #
9
+ # class UserGrid < ApplicationGrid
10
+ # scope do
11
+ # User.order("users.created_at desc").joins(:group)
12
+ # end
13
+ #
14
+ # column(:name)
15
+ # column(:group, order: "groups.name") do
16
+ # self.group.name
17
+ # end
18
+ # column(:active, header: "Activated") do |user|
19
+ # !user.disabled
20
+ # end
21
+ # end
22
+ #
23
+ # Each column is used to generate data for the grid.
24
+ #
25
+ # To create a grid displaying all users:
26
+ #
27
+ # grid = UserGrid.new
28
+ # grid.header # => ["Group", "Name", "Disabled"]
29
+ # grid.rows # => [
30
+ # # ["Steve", "Spammers", true],
31
+ # # ["John", "Spoilers", true],
32
+ # # ["Berry", "Good people", false]
33
+ # # ]
34
+ # grid.data # => Header & Rows
35
+ # grid.data_hash # => [
36
+ # # { name: "Steve", group: "Spammers", active: true },
37
+ # # { name: "John", group: "Spoilers", active: true },
38
+ # # { name: "Berry", group: "Good people", active: false },
39
+ # # ]
40
+ # }
41
+ #
42
+ # ## Column Value
43
+ #
44
+ # The value of a column can be defined by passing a block to `Datagrid.column`.
45
+ #
46
+ # ### Basic Column Value
47
+ #
48
+ # If no block is provided, the column value is generated automatically by sending the column name method to the model.
49
+ #
50
+ # column(:name) # => asset.name
51
+ #
52
+ # Using <tt>instance_eval</tt>:
53
+ #
54
+ # column(:completed) { completed? }
55
+ #
56
+ # Using the asset as an argument:
57
+ #
58
+ # column(:completed) { |asset| asset.completed? }
59
+ #
60
+ # ### Advanced Column Value
61
+ #
62
+ # You can also pass the Datagrid object itself to define more complex column values.
63
+ #
64
+ # Using filters with columns:
65
+ #
66
+ # filter(:category) do |value|
67
+ # where("category LIKE '%#{value}%'")
68
+ # end
69
+ #
70
+ # column(:exactly_matches_category) do |asset, grid|
71
+ # asset.category == grid.category
72
+ # end
73
+ #
74
+ # Combining columns:
75
+ #
76
+ # column(:total_sales) do |merchant|
77
+ # merchant.purchases.sum(:subtotal)
78
+ # end
79
+ # column(:number_of_sales) do |merchant|
80
+ # merchant.purchases.count
81
+ # end
82
+ # column(:average_order_value) do |_, _, row|
83
+ # row.total_sales / row.number_of_sales
84
+ # end
85
+ #
86
+ # ## Using Database Expressions
87
+ #
88
+ # Columns can use database expressions to directly manipulate data in the database.
89
+ #
90
+ # column(:count_of_users, 'count(user_id)')
91
+ # column(:uppercase_name, 'upper(name)')
92
+ #
93
+ # ## HTML Columns
94
+ #
95
+ # Columns can have different formats for HTML and non-HTML representations.
96
+ #
97
+ # column(:name) do |asset|
98
+ # format(asset.name) do |value|
99
+ # content_tag(:strong, value)
100
+ # end
101
+ # end
102
+ #
103
+ # ## Column Value Cache
104
+ #
105
+ # Enables grid-level caching for column values.
106
+ #
107
+ # self.cached = true
108
+ #
109
+ # ## Ordering
110
+ #
111
+ # Columns can specify SQL ordering expressions using the `:order` and `:order_desc` options.
112
+ #
113
+ # Basic ordering:
114
+ #
115
+ # column(:group_name, order: "groups.name") { self.group.name }
116
+ #
117
+ # In example above descending order is automatically inherited from ascending order specified.
118
+ # When such default is not enough, specify both options together:
119
+ #
120
+ # column(
121
+ # :priority,
122
+ # # models with null priority are always on bottom
123
+ # # no matter if sorting ascending or descending
124
+ # order: "priority is not null desc, priority",
125
+ # order_desc: "priority is not null desc, priority desc"
126
+ # )
127
+ #
128
+ # Disable order like this:
129
+ #
130
+ # column(:title, order: false)
131
+ #
132
+ # Order by joined table
133
+ # Allows to join specified table only when order is enabled
134
+ # for performance:
135
+ #
136
+ # column(:profile_updated_at, order: proc { |scope|
137
+ # scope.joins(:profile).order("profiles.updated_at")
138
+ # }) do |model|
139
+ # model.profile.updated_at.to_date
140
+ # end
141
+ #
142
+ # Order by a calculated value
143
+ #
144
+ # column(
145
+ # :duration_request,
146
+ # order: "(requests.finished_at - requests.accepted_at)"
147
+ # ) do |model|
148
+ # Time.at(model.finished_at - model.accepted_at).strftime("%H:%M:%S")
149
+ # end
150
+ #
151
+ # ## Default Column Options
152
+ #
153
+ # Default options for all columns in a grid can be set using `default_column_options`.
154
+ #
155
+ # self.default_column_options = { order: false }
156
+ #
157
+ # ## Columns Visibility
158
+ #
159
+ # Columns can be dynamically shown or hidden based on the grid's `column_names` accessor.
160
+ #
161
+ # grid.column_names = [:id, :name]
162
+ #
163
+ # ## Dynamic Columns
164
+ #
165
+ # Columns can be defined dynamically on a grid instance or based on data.
166
+ #
167
+ # Adding a dynamic column:
168
+ #
169
+ # grid.column(:extra_data) do |model|
170
+ # model.extra_data
171
+ # end
172
+ #
173
+ # ## Localization
174
+ #
175
+ # Column headers can be localized using the `:header` option or through i18n files.
176
+ #
177
+ # column(:active, header: Proc.new { I18n.t("activated") })
178
+ #
179
+ # ## Preloading Associations
180
+ #
181
+ # Preload database associations for better performance.
182
+ #
183
+ # Automatic association preloading:
184
+ #
185
+ # column(:group) do |user|
186
+ # user.group.name
187
+ # end
188
+ #
189
+ # Custom preloading:
190
+ #
191
+ # column(:account_name, preload: { |s| s.includes(:account) })
192
+ #
193
+ # ## Decorator
194
+ #
195
+ # A decorator or presenter class can be used around each object in the `scope`.
196
+ #
197
+ # decorate { UserPresenter }
198
+ # column(:created_at) do |presenter|
199
+ # presenter.user.created_at
200
+ # end
6
201
  module Columns
7
202
  require "datagrid/columns/column"
8
203
 
9
- # @!method default_column_options=
10
- # @param value [Hash] default options passed to #column method call
204
+ # @!method default_column_options=(value)
205
+ # @param [Hash] value default options passed to #column method call
11
206
  # @return [Hash] default options passed to #column method call
12
207
  # @example
13
208
  # # Disable default order
@@ -19,8 +214,8 @@ module Datagrid
19
214
  # @return [Hash]
20
215
  # @see #default_column_options=
21
216
 
22
- # @!method batch_size=
23
- # @param value [Integer] Specify a default batch size when generating CSV or just data. Default: 1000
217
+ # @!method batch_size=(value)
218
+ # @param [Integer] value Specify a default batch size when generating CSV or just data. Default: 1000
24
219
  # @return [Integer] Specify a default batch size when generating CSV or just data.
25
220
  # @example
26
221
  # self.batch_size = 500
@@ -33,8 +228,9 @@ module Datagrid
33
228
  # @see #batch_size=
34
229
 
35
230
  # @visibility private
231
+ # @param [Object] base
36
232
  def self.included(base)
37
- base.extend ClassMethods
233
+ base.extend ClassMethods
38
234
  base.class_eval do
39
235
  include Datagrid::Core
40
236
 
@@ -44,11 +240,9 @@ module Datagrid
44
240
  class_attribute :cached, default: false
45
241
  class_attribute :decorator, instance_writer: false
46
242
  end
47
- base.include InstanceMethods
48
243
  end
49
244
 
50
245
  module ClassMethods
51
-
52
246
  # @param data [Boolean] if true returns only columns with data representation. Default: false.
53
247
  # @param html [Boolean] if true returns only columns with html columns. Default: false.
54
248
  # @param column_names [Array<String>] list of column names if you want to limit data only to specified columns
@@ -63,7 +257,7 @@ module Datagrid
63
257
  #
64
258
  # @param name [Symbol] column name
65
259
  # @param query [String, nil] a string representing the query to select this column (supports only ActiveRecord)
66
- # @param options [Hash<Symbol, Object>] hash of options
260
+ # @param options [Hash{Symbol => Object}] hash of options
67
261
  # @param block [Block] proc to calculate a column value
68
262
  # @return [Datagrid::Columns::Column]
69
263
  #
@@ -72,7 +266,7 @@ module Datagrid
72
266
  # * <tt>html</tt> - determines if current column should be present in html table and how is it formatted
73
267
  # * <tt>order</tt> - determines if this column could be sortable and how.
74
268
  # The value of order is explicitly passed to ORM ordering method.
75
- # Ex: <tt>"created_at, id"</tt> for ActiveRecord, <tt>[:created_at, :id]</tt> for Mongoid
269
+ # Example: <tt>"created_at, id"</tt> for ActiveRecord, <tt>[:created_at, :id]</tt> for Mongoid
76
270
  # * <tt>order_desc</tt> - determines a descending order for given column
77
271
  # (only in case when <tt>:order</tt> can not be easily reversed by ORM)
78
272
  # * <tt>order_by_value</tt> - used in case it is easier to perform ordering at ruby level not on database level.
@@ -86,6 +280,8 @@ module Datagrid
86
280
  # * <tt>if</tt> - the column is shown if the reult of calling this argument is true
87
281
  # * <tt>unless</tt> - the column is shown unless the reult of calling this argument is true
88
282
  # * <tt>preload</tt> - spefies which associations of the scope should be preloaded for this column
283
+ # * `tag_options` - specify HTML attributes to be set for `<td>` or `<th>` of a column
284
+ # Example: `{ class: "content-align-right", "data-group": "statistics" }`
89
285
  #
90
286
  # @see https://github.com/bogdan/datagrid/wiki/Columns
91
287
  def column(name, query = nil, **options, &block)
@@ -116,7 +312,7 @@ module Datagrid
116
312
  # @example
117
313
  # column(:name) do |model|
118
314
  # format(model.name) do |value|
119
- # content_tag(:strong, value)
315
+ # tag.strong(value)
120
316
  # end
121
317
  # end
122
318
  def format(value, &block)
@@ -147,20 +343,25 @@ module Datagrid
147
343
  end
148
344
  return self.decorator = block unless model
149
345
  return model unless decorator
346
+
150
347
  presenter = ::Datagrid::Utils.apply_args(model, &decorator)
151
- presenter = presenter.is_a?(Class) ? presenter.new(model) : presenter
348
+ presenter = presenter.new(model) if presenter.is_a?(Class)
152
349
  block_given? ? yield(presenter) : presenter
153
350
  end
154
351
 
155
352
  # @!visibility private
156
353
  def inherited(child_class)
157
- super(child_class)
158
- child_class.columns_array = self.columns_array.clone
354
+ super
355
+ child_class.columns_array = columns_array.clone
159
356
  end
160
357
 
161
358
  # @!visibility private
162
359
  def filter_columns(columns_array, *names, data: false, html: false)
163
360
  names.compact!
361
+ if names.size >= 1 && names.all? { |n| n.is_a?(Datagrid::Columns::Column) && n.grid_class == self.class }
362
+ return names
363
+ end
364
+
164
365
  names.map!(&:to_sym)
165
366
  columns_array.select do |column|
166
367
  (!data || column.data?) &&
@@ -173,7 +374,7 @@ module Datagrid
173
374
  def define_column(columns, name, query = nil, **options, &block)
174
375
  check_scope_defined!("Scope should be defined before columns")
175
376
  block ||= lambda do |model|
176
- model.send(name)
377
+ model.public_send(name)
177
378
  end
178
379
  position = Datagrid::Utils.extract_position_from_options(columns, options)
179
380
  column = Datagrid::Columns::Column.new(
@@ -184,344 +385,356 @@ module Datagrid
184
385
  end
185
386
 
186
387
  # @!visibility private
187
- def find_column_by_name(columns,name)
388
+ def find_column_by_name(columns, name)
188
389
  return name if name.is_a?(Datagrid::Columns::Column)
390
+
189
391
  columns.find do |col|
190
392
  col.name.to_sym == name.to_sym
191
393
  end
192
394
  end
193
-
194
395
  end
195
396
 
196
- module InstanceMethods
397
+ # @!visibility private
398
+ def assets
399
+ append_column_preload(
400
+ driver.append_column_queries(
401
+ super, columns.select(&:query),
402
+ ),
403
+ )
404
+ end
197
405
 
198
- # @!visibility private
199
- def assets
200
- append_column_preload(
201
- driver.append_column_queries(
202
- super, columns.select(&:query)
203
- )
204
- )
205
- end
406
+ # @param column_names [Array<String>] list of column names if you want to limit data only to specified columns
407
+ # @return [Array<String>] human readable column names. See also "Localization" section
408
+ def header(*column_names)
409
+ data_columns(*column_names).map(&:header)
410
+ end
206
411
 
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)
412
+ # @param asset [Object] asset from datagrid scope
413
+ # @param column_names [Array<String>] list of column names if you want to limit data only to specified columns
414
+ # @return [Array<Object>] column values for given asset
415
+ def row_for(asset, *column_names)
416
+ data_columns(*column_names).map do |column|
417
+ data_value(column, asset)
211
418
  end
419
+ end
212
420
 
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
421
+ # @param asset [Object] asset from datagrid scope
422
+ # @return [Hash] A mapping where keys are column names and values are column values for the given asset
423
+ def hash_for(asset)
424
+ result = {}
425
+ data_columns.each do |column|
426
+ result[column.name] = data_value(column, asset)
220
427
  end
428
+ result
429
+ end
221
430
 
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
431
+ # @param column_names [Array<String>] list of column names if you want to limit data only to specified columns
432
+ # @return [Array<Array<Object>>] with data for each row in datagrid assets without header
433
+ def rows(*column_names)
434
+ map_with_batches do |asset|
435
+ row_for(asset, *column_names)
230
436
  end
437
+ end
231
438
 
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
238
- end
439
+ # @param column_names [Array<String>] list of column names if you want to limit data only to specified columns
440
+ # @return [Array<Array<Object>>] data for each row in datagrid assets with header.
441
+ def data(*column_names)
442
+ rows(*column_names).unshift(header(*column_names))
443
+ end
239
444
 
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))
445
+ # @return [Array<Hash{Symbol => Object}>] an array of hashes representing the rows in the filtered datagrid relation
446
+ #
447
+ # @example
448
+ # class MyGrid
449
+ # scope { Model }
450
+ # column(:id)
451
+ # column(:name)
452
+ # end
453
+ #
454
+ # Model.create!(name: "One")
455
+ # Model.create!(name: "Two")
456
+ #
457
+ # MyGrid.new.data_hash # => [{name: "One"}, {name: "Two"}]
458
+ def data_hash
459
+ map_with_batches do |asset|
460
+ hash_for(asset)
244
461
  end
462
+ end
245
463
 
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)
464
+ # @param column_names [Array<String>]
465
+ # @param options [Hash] CSV generation options
466
+ # @return [String] a CSV representation of the data in the grid
467
+ #
468
+ # @example
469
+ # grid.to_csv
470
+ # grid.to_csv(:id, :name)
471
+ # grid.to_csv(col_sep: ';')
472
+ def to_csv(*column_names, **options)
473
+ require "csv"
474
+ CSV.generate(
475
+ headers: header(*column_names),
476
+ write_headers: true,
477
+ **options,
478
+ ) do |csv|
479
+ each_with_batches do |asset|
480
+ csv << row_for(asset, *column_names)
263
481
  end
264
482
  end
483
+ end
265
484
 
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
284
- end
485
+ # @param column_names [Array<Symbol, String>]
486
+ # @param [Boolean] data return only data columns
487
+ # @param [Boolean] html return only HTML columns
488
+ # @return [Array<Datagrid::Columns::Column>] all columns selected in grid instance
489
+ # @example
490
+ # MyGrid.new.columns # => all defined columns
491
+ # grid = MyGrid.new(column_names: [:id, :name])
492
+ # grid.columns # => id and name columns
493
+ # grid.columns(:id, :category) # => id and category column
494
+ def columns(*column_names, data: false, html: false)
495
+ self.class.filter_columns(
496
+ columns_array, *column_names, data: data, html: html,
497
+ ).select do |column|
498
+ column.enabled?(self)
285
499
  end
500
+ end
286
501
 
502
+ # @param column_names [Array<String, Symbol>] list of column names
503
+ # if you want to limit data only to specified columns
504
+ # @param [Boolean] html return only HTML columns
505
+ # @return [Array<Datagrid::Columns::Column>] columns that can be represented in plain data(non-html) way
506
+ def data_columns(*column_names, html: false)
507
+ columns(*column_names, html: html, data: true)
508
+ end
287
509
 
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
301
- end
510
+ # @param column_names [Array<String>] list of column names if you want to limit data only to specified columns
511
+ # @param [Boolean] data return only data columns
512
+ # @return all columns that can be represented in HTML table
513
+ def html_columns(*column_names, data: false)
514
+ columns(*column_names, data: data, html: true)
515
+ end
302
516
 
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
517
+ # Finds a column definition by name
518
+ # @param name [String, Symbol] column name to be found
519
+ # @return [Datagrid::Columns::Column, nil]
520
+ def column_by_name(name)
521
+ self.class.find_column_by_name(columns_array, name)
522
+ end
308
523
 
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)
524
+ # Gives ability to have a different formatting for CSV and HTML column value.
525
+ #
526
+ # @example
527
+ # column(:name) do |model|
528
+ # format(model.name) do |value|
529
+ # tag.strong(value)
530
+ # end
531
+ # end
532
+ #
533
+ # column(:company) do |model|
534
+ # format(model.company.name) do
535
+ # render partial: "company_with_logo", locals: {company: model.company }
536
+ # end
537
+ # end
538
+ # @return [Datagrid::Columns::Column::ResponseFormat] Format object
539
+ def format(value, &block)
540
+ if block_given?
541
+ self.class.format(value, &block)
542
+ else
543
+ # don't override Object#format method
544
+ super
313
545
  end
546
+ end
314
547
 
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
548
+ # @return [Datagrid::Columns::DataRow] an object representing a grid row.
549
+ # @example
550
+ # class MyGrid
551
+ # scope { User }
552
+ # column(:id)
553
+ # column(:name)
554
+ # column(:number_of_purchases) do |user|
555
+ # user.purchases.count
556
+ # end
557
+ # end
558
+ #
559
+ # row = MyGrid.new.data_row(User.last)
560
+ # row.id # => user.id
561
+ # row.number_of_purchases # => user.purchases.count
562
+ def data_row(asset)
563
+ ::Datagrid::Columns::DataRow.new(self, asset)
564
+ end
321
565
 
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
344
- end
566
+ # Defines a column at instance level
567
+ #
568
+ # @see Datagrid::Columns::ClassMethods#column
569
+ def column(name, query = nil, **options, &block)
570
+ self.class.define_column(columns_array, name, query, **options, &block)
571
+ end
345
572
 
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
573
+ # @!visibility private
574
+ def initialize(*)
575
+ self.columns_array = self.class.columns_array.clone
576
+ super
577
+ end
363
578
 
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)
579
+ # @return [Array<Datagrid::Columns::Column>] all columns
580
+ # that are possible to be displayed for the current grid object
581
+ #
582
+ # @example
583
+ # class MyGrid
584
+ # filter(:search) {|scope, value| scope.full_text_search(value)}
585
+ # column(:id)
586
+ # column(:name, mandatory: true)
587
+ # column(:search_match, if: proc {|grid| grid.search.present? }) do |model, grid|
588
+ # search_match_line(model.searchable_content, grid.search)
589
+ # end
590
+ # end
591
+ #
592
+ # grid = MyGrid.new
593
+ # grid.columns # => [ #<Column:name> ]
594
+ # grid.available_columns # => [ #<Column:id>, #<Column:name> ]
595
+ # grid.search = "keyword"
596
+ # grid.available_columns # => [ #<Column:id>, #<Column:name>, #<Column:search_match> ]
597
+ def available_columns
598
+ columns_array.select do |column|
599
+ column.enabled?(self)
369
600
  end
601
+ end
370
602
 
371
- # @!visibility private
372
- def initialize(*)
373
- self.columns_array = self.class.columns_array.clone
374
- super
375
- end
603
+ # @return [Object] a cell data value for given column name and asset
604
+ def data_value(column_name, asset)
605
+ column = column_by_name(column_name)
606
+ cache(column, asset, :data_value) do
607
+ raise "no data value for #{column.name} column" unless column.data?
376
608
 
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
609
+ result = generic_value(column, asset)
610
+ result.is_a?(Datagrid::Columns::Column::ResponseFormat) ? result.call_data : result
398
611
  end
612
+ end
399
613
 
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?
614
+ # @return [Object] a cell HTML value for given column name and asset and view context
615
+ def html_value(column_name, context, asset)
616
+ column = column_by_name(column_name)
617
+ cache(column, asset, :html_value) do
618
+ if column.html? && column.html_block
619
+ value_from_html_block(context, asset, column)
620
+ else
405
621
  result = generic_value(column, asset)
406
- result.is_a?(Datagrid::Columns::Column::ResponseFormat) ? result.call_data : result
622
+ result.is_a?(Datagrid::Columns::Column::ResponseFormat) ? result.call_html(context) : result
407
623
  end
408
624
  end
625
+ end
409
626
 
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
420
- end
421
- end
422
-
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}"
434
- end
627
+ # @return [Object] a decorated version of given model if decorator is specified or the model otherwise.
628
+ def decorate(model)
629
+ self.class.decorate(model)
630
+ end
435
631
 
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
632
+ # @!visibility private
633
+ def generic_value(column, model)
634
+ cache(column, model, :generic_value) do
635
+ presenter = decorate(model)
636
+ unless column.enabled?(self)
637
+ raise Datagrid::ColumnUnavailableError, "Column #{column.name} disabled for #{inspect}"
441
638
  end
442
639
 
640
+ if column.data_block.arity >= 1
641
+ Datagrid::Utils.apply_args(presenter, self, data_row(model), &column.data_block)
642
+ else
643
+ presenter.instance_eval(&column.data_block)
644
+ end
443
645
  end
646
+ end
444
647
 
445
- protected
648
+ # @!visibility private
649
+ def reset
650
+ super
651
+ @cache = {}
652
+ end
446
653
 
447
- def append_column_preload(relation)
448
- columns.inject(relation) do |current, column|
449
- column.append_preload(current)
450
- end
451
- end
654
+ protected
452
655
 
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
656
+ def append_column_preload(relation)
657
+ columns.inject(relation) do |current, column|
658
+ column.append_preload(current)
466
659
  end
660
+ end
467
661
 
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."
662
+ def cache(column, asset, type)
663
+ @cache ||= {}
664
+ unless cached?
665
+ @cache.clear
666
+ return yield
476
667
  end
668
+ key = cache_key(asset)
669
+ raise(Datagrid::CacheKeyError, "Datagrid Cache key is #{key.inspect} for #{asset.inspect}.") unless key
477
670
 
671
+ @cache[column.name] ||= {}
672
+ @cache[column.name][key] ||= {}
673
+ @cache[column.name][key][type] ||= yield
674
+ end
478
675
 
479
- def map_with_batches(&block)
480
- result = []
481
- each_with_batches do |asset|
482
- result << block.call(asset)
483
- end
484
- result
676
+ def cache_key(asset)
677
+ if cached.respond_to?(:call)
678
+ cached.call(asset)
679
+ else
680
+ driver.default_cache_key(asset)
681
+ end
682
+ rescue NotImplementedError
683
+ raise Datagrid::ConfigurationError,
684
+ <<~MSG
685
+ #{self} is setup to use cache. But there was appropriate cache key found for #{asset.inspect}.
686
+ MSG
687
+ end
688
+
689
+ def map_with_batches(&block)
690
+ result = []
691
+ each_with_batches do |asset|
692
+ result << block.call(asset)
485
693
  end
694
+ result
695
+ end
486
696
 
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
697
+ def each_with_batches(&block)
698
+ if batch_size&.positive?
699
+ driver.batch_each(assets, batch_size, &block)
700
+ else
701
+ assets.each(&block)
493
702
  end
703
+ end
494
704
 
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
705
+ def value_from_html_block(context, asset, column)
706
+ args = []
707
+ remaining_arity = column.html_block.arity
708
+ remaining_arity = 1 if remaining_arity.negative?
499
709
 
500
- asset = decorate(asset)
710
+ asset = decorate(asset)
501
711
 
502
- if column.data?
503
- args << data_value(column, asset)
504
- remaining_arity -= 1
505
- end
712
+ if column.data?
713
+ args << data_value(column, asset)
714
+ remaining_arity -= 1
715
+ end
506
716
 
507
- args << asset if remaining_arity > 0
508
- args << self if remaining_arity > 1
717
+ args << asset if remaining_arity.positive?
718
+ args << self if remaining_arity > 1
509
719
 
510
- context.instance_exec(*args, &column.html_block)
511
- end
720
+ context.instance_exec(*args, &column.html_block)
512
721
  end
513
722
 
514
723
  # Object representing a single row of data when building a datagrid table
515
- # @see Datagrid::Columns::InstanceMethods#data_row
724
+ # @see Datagrid::Columns#data_row
516
725
  class DataRow < BasicObject
517
726
  def initialize(grid, model)
518
727
  @grid = grid
519
728
  @model = model
520
729
  end
521
730
 
522
- def method_missing(meth, *args, &blk)
731
+ def method_missing(meth, *_args)
523
732
  @grid.data_value(meth, @model)
524
733
  end
734
+
735
+ def respond_to_missing?(meth, include_private = false)
736
+ !!@grid.column_by_name(meth) || super
737
+ end
525
738
  end
526
739
  end
527
740
  end