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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +31 -7
- data/{Readme.markdown → README.md} +46 -29
- data/app/assets/stylesheets/datagrid.css +145 -0
- data/app/views/datagrid/_enum_checkboxes.html.erb +5 -3
- data/app/views/datagrid/_form.html.erb +4 -5
- data/app/views/datagrid/_head.html.erb +26 -3
- data/app/views/datagrid/_range_filter.html.erb +5 -3
- data/app/views/datagrid/_row.html.erb +12 -1
- data/app/views/datagrid/_table.html.erb +4 -4
- data/datagrid.gemspec +8 -8
- data/lib/datagrid/active_model.rb +9 -17
- data/lib/datagrid/base.rb +39 -0
- data/lib/datagrid/column_names_attribute.rb +12 -12
- data/lib/datagrid/columns/column.rb +155 -133
- data/lib/datagrid/columns.rb +495 -282
- data/lib/datagrid/configuration.rb +23 -10
- data/lib/datagrid/core.rb +184 -150
- data/lib/datagrid/deprecated_object.rb +20 -0
- data/lib/datagrid/drivers/abstract_driver.rb +13 -25
- data/lib/datagrid/drivers/active_record.rb +24 -26
- data/lib/datagrid/drivers/array.rb +26 -17
- data/lib/datagrid/drivers/mongo_mapper.rb +15 -14
- data/lib/datagrid/drivers/mongoid.rb +16 -18
- data/lib/datagrid/drivers/sequel.rb +14 -19
- data/lib/datagrid/drivers.rb +2 -1
- data/lib/datagrid/engine.rb +11 -3
- data/lib/datagrid/filters/base_filter.rb +166 -142
- data/lib/datagrid/filters/boolean_filter.rb +19 -5
- data/lib/datagrid/filters/date_filter.rb +33 -35
- data/lib/datagrid/filters/date_time_filter.rb +24 -16
- data/lib/datagrid/filters/default_filter.rb +9 -3
- data/lib/datagrid/filters/dynamic_filter.rb +151 -105
- data/lib/datagrid/filters/enum_filter.rb +43 -19
- data/lib/datagrid/filters/extended_boolean_filter.rb +39 -27
- data/lib/datagrid/filters/float_filter.rb +16 -5
- data/lib/datagrid/filters/integer_filter.rb +21 -10
- data/lib/datagrid/filters/ranged_filter.rb +66 -45
- data/lib/datagrid/filters/select_options.rb +58 -49
- data/lib/datagrid/filters/string_filter.rb +9 -4
- data/lib/datagrid/filters.rb +234 -106
- data/lib/datagrid/form_builder.rb +116 -128
- data/lib/datagrid/generators/scaffold.rb +185 -0
- data/lib/datagrid/generators/views.rb +20 -0
- data/lib/datagrid/helper.rb +397 -22
- data/lib/datagrid/ordering.rb +81 -87
- data/lib/datagrid/rspec.rb +8 -12
- data/lib/datagrid/utils.rb +42 -38
- data/lib/datagrid/version.rb +3 -1
- data/lib/datagrid.rb +18 -28
- data/templates/base.rb.erb +33 -7
- data/templates/grid.rb.erb +1 -1
- metadata +18 -19
- data/app/assets/stylesheets/datagrid.sass +0 -134
- data/lib/datagrid/filters/composite_filters.rb +0 -49
- data/lib/datagrid/renderer.rb +0 -157
- data/lib/datagrid/scaffold.rb +0 -129
- data/lib/tasks/datagrid_tasks.rake +0 -15
- data/templates/controller.rb.erb +0 -6
- data/templates/index.html.erb +0 -5
data/lib/datagrid/columns.rb
CHANGED
@@ -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
|
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
|
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
|
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
|
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
|
-
#
|
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
|
-
#
|
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.
|
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
|
158
|
-
child_class.columns_array =
|
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.
|
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
|
-
|
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
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
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
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
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
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
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
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
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
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
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
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
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
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
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
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
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
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
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
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
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
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
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
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
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
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
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
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
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
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
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
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
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
|
-
|
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
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
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.
|
622
|
+
result.is_a?(Datagrid::Columns::Column::ResponseFormat) ? result.call_html(context) : result
|
407
623
|
end
|
408
624
|
end
|
625
|
+
end
|
409
626
|
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
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
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
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
|
-
|
648
|
+
# @!visibility private
|
649
|
+
def reset
|
650
|
+
super
|
651
|
+
@cache = {}
|
652
|
+
end
|
446
653
|
|
447
|
-
|
448
|
-
columns.inject(relation) do |current, column|
|
449
|
-
column.append_preload(current)
|
450
|
-
end
|
451
|
-
end
|
654
|
+
protected
|
452
655
|
|
453
|
-
|
454
|
-
|
455
|
-
|
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
|
-
|
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."
|
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
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
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
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
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
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
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
|
-
|
710
|
+
asset = decorate(asset)
|
501
711
|
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
712
|
+
if column.data?
|
713
|
+
args << data_value(column, asset)
|
714
|
+
remaining_arity -= 1
|
715
|
+
end
|
506
716
|
|
507
|
-
|
508
|
-
|
717
|
+
args << asset if remaining_arity.positive?
|
718
|
+
args << self if remaining_arity > 1
|
509
719
|
|
510
|
-
|
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
|
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, *
|
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
|