datagrid 1.8.2 → 2.0.0
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/.yardopts +2 -0
- data/CHANGELOG.md +11 -1
- data/{Readme.markdown → README.md} +44 -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 -4
- 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 -7
- data/lib/datagrid/active_model.rb +9 -17
- data/lib/datagrid/base.rb +39 -0
- data/lib/datagrid/column_names_attribute.rb +9 -11
- data/lib/datagrid/columns/column.rb +155 -133
- data/lib/datagrid/columns.rb +325 -115
- data/lib/datagrid/configuration.rb +33 -4
- data/lib/datagrid/core.rb +89 -54
- data/lib/datagrid/deprecated_object.rb +20 -0
- data/lib/datagrid/drivers/abstract_driver.rb +12 -23
- data/lib/datagrid/drivers/active_record.rb +24 -26
- data/lib/datagrid/drivers/array.rb +22 -14
- data/lib/datagrid/drivers/mongo_mapper.rb +15 -14
- data/lib/datagrid/drivers/mongoid.rb +15 -17
- 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 -143
- 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 -31
- data/lib/datagrid/filters/float_filter.rb +15 -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 +204 -79
- data/lib/datagrid/form_builder.rb +116 -128
- data/lib/datagrid/generators/scaffold.rb +184 -0
- data/lib/datagrid/generators/views.rb +20 -0
- data/lib/datagrid/helper.rb +436 -69
- data/lib/datagrid/ordering.rb +26 -29
- data/lib/datagrid/rspec.rb +6 -10
- data/lib/datagrid/utils.rb +37 -30
- data/lib/datagrid/version.rb +3 -1
- data/lib/datagrid.rb +8 -28
- data/templates/base.rb.erb +6 -4
- data/templates/grid.rb.erb +1 -1
- metadata +17 -17
- 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/filters.rb
CHANGED
@@ -1,8 +1,135 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "active_support/core_ext/class/attribute"
|
2
4
|
|
3
5
|
module Datagrid
|
6
|
+
# Defines the accessible attribute that is used to filter
|
7
|
+
# the scope by the specified value with specified code.
|
8
|
+
#
|
9
|
+
# class UserGrid < ApplicationGrid
|
10
|
+
# scope do
|
11
|
+
# User
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# filter(:name)
|
15
|
+
# filter(:posts_count, :integer) do |value|
|
16
|
+
# self.where(["posts_count >= ?", value])
|
17
|
+
# end
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# Each filter becomes a grid attribute.
|
21
|
+
#
|
22
|
+
# To create a grid displaying all users with the name 'John' who have more than zero posts:
|
23
|
+
#
|
24
|
+
# grid = UserGrid.new(posts_count: 1, name: "John")
|
25
|
+
# grid.assets # SELECT * FROM users WHERE users.posts_count > 1 AND name = 'John'
|
26
|
+
#
|
27
|
+
# # Filter Block
|
28
|
+
#
|
29
|
+
# Filter blocks should always return a chainable ORM object (e.g., `ActiveRecord::Relation`) rather than an `Array`.
|
30
|
+
#
|
31
|
+
# Filter blocks should have at least one argument representing the value assigned to the grid object attribute:
|
32
|
+
#
|
33
|
+
# filter(:name, :string) # { |value| where(name: value) }
|
34
|
+
#
|
35
|
+
# You can pass additional arguments:
|
36
|
+
#
|
37
|
+
# filter(:name, :string) { |value, scope| scope.where("name ilike ?", "%#{value}%") }
|
38
|
+
# filter(:name, :string) do |value, scope, grid|
|
39
|
+
# scope.where("name #{grid.predicate} ?", "%#{value}%")
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
# # Filter Types
|
43
|
+
#
|
44
|
+
# Filters perform automatic type conversion. Supported filter types include:
|
45
|
+
#
|
46
|
+
# - `default`
|
47
|
+
# - `date`
|
48
|
+
# - `datetime`
|
49
|
+
# - `enum`
|
50
|
+
# - `boolean`
|
51
|
+
# - `xboolean`
|
52
|
+
# - `integer`
|
53
|
+
# - `float`
|
54
|
+
# - `string`
|
55
|
+
# - `dynamic`
|
56
|
+
#
|
57
|
+
# ## Default
|
58
|
+
#
|
59
|
+
# `:default` - Leaves the value as is.
|
60
|
+
#
|
61
|
+
# ## Date
|
62
|
+
#
|
63
|
+
# `:date` - Converts value to a date. Supports the `:range` option to accept date ranges.
|
64
|
+
#
|
65
|
+
# filter(:created_at, :date, range: true, default: proc { 1.month.ago.to_date..Date.today })
|
66
|
+
#
|
67
|
+
# ## Datetime
|
68
|
+
#
|
69
|
+
# `:datetime` - Converts value to a timestamp. Supports the `:range` option to accept time ranges.
|
70
|
+
#
|
71
|
+
# filter(:created_at, :datetime, range: true, default: proc { 1.hour.ago..Time.now })
|
72
|
+
#
|
73
|
+
# ## Enum
|
74
|
+
#
|
75
|
+
# `:enum` - For collection selection with options like `:select` and `:multiple`.
|
76
|
+
#
|
77
|
+
# filter(:user_type, :enum, select: ['Admin', 'Customer', 'Manager'])
|
78
|
+
# filter(:category_id, :enum, select: proc { Category.all.map { |c| [c.name, c.id] } }, multiple: true)
|
79
|
+
#
|
80
|
+
# ## Boolean
|
81
|
+
#
|
82
|
+
# `:boolean` - Converts value to `true` or `false`.
|
83
|
+
#
|
84
|
+
# ## Xboolean
|
85
|
+
#
|
86
|
+
# `:xboolean` - Subtype of `enum` filter that provides "Yes", "No", and "Any" options.
|
87
|
+
#
|
88
|
+
# filter(:active, :xboolean)
|
89
|
+
#
|
90
|
+
# ## Integer
|
91
|
+
#
|
92
|
+
# `:integer` - Converts value to an integer. Supports the `:range` option.
|
93
|
+
#
|
94
|
+
# filter(:posts_count, :integer, range: true, default: (1..nil))
|
95
|
+
#
|
96
|
+
# ## String
|
97
|
+
#
|
98
|
+
# `:string` - Converts value to a string.
|
99
|
+
#
|
100
|
+
# filter(:email, :string)
|
101
|
+
#
|
102
|
+
# ## Dynamic
|
103
|
+
#
|
104
|
+
# Provides a builder for dynamic SQL conditions.
|
105
|
+
#
|
106
|
+
# filter(:condition1, :dynamic)
|
107
|
+
# filter(:condition2, :dynamic)
|
108
|
+
# UsersGrid.new(condition1: [:name, "=~", "John"], condition2: [:posts_count, ">=", 1])
|
109
|
+
# UsersGrid.assets # SELECT * FROM users WHERE name like '%John%' and posts_count >= 1
|
110
|
+
#
|
111
|
+
# # Filter Options
|
112
|
+
#
|
113
|
+
# Options that can be passed to any filter:
|
114
|
+
#
|
115
|
+
# - `:header` - Human-readable filter name (default: generated from the filter name).
|
116
|
+
# - `:default` - Default filter value (default: `nil`).
|
117
|
+
# - `:multiple` - Allows multiple values (default: `false`).
|
118
|
+
# - `:allow_nil` - Whether to apply the filter when the value is `nil` (default: `false`).
|
119
|
+
# - `:allow_blank` - Whether to apply the filter when the value is blank (default: `false`).
|
120
|
+
#
|
121
|
+
# Example:
|
122
|
+
#
|
123
|
+
# filter(:id, :integer, header: "Identifier")
|
124
|
+
# filter(:created_at, :date, range: true, default: proc { 1.month.ago.to_date..Date.today })
|
125
|
+
#
|
126
|
+
# # Localization
|
127
|
+
#
|
128
|
+
# Filter labels can be localized or specified via the `:header` option:
|
129
|
+
#
|
130
|
+
# filter(:created_at, :date, header: "Creation date")
|
131
|
+
# filter(:created_at, :date, header: proc { I18n.t("creation_date") })
|
4
132
|
module Filters
|
5
|
-
|
6
133
|
require "datagrid/filters/base_filter"
|
7
134
|
require "datagrid/filters/enum_filter"
|
8
135
|
require "datagrid/filters/extended_boolean_filter"
|
@@ -11,49 +138,46 @@ module Datagrid
|
|
11
138
|
require "datagrid/filters/date_time_filter"
|
12
139
|
require "datagrid/filters/default_filter"
|
13
140
|
require "datagrid/filters/integer_filter"
|
14
|
-
require "datagrid/filters/composite_filters"
|
15
141
|
require "datagrid/filters/string_filter"
|
16
142
|
require "datagrid/filters/float_filter"
|
17
143
|
require "datagrid/filters/dynamic_filter"
|
18
144
|
|
145
|
+
# @!visibility private
|
19
146
|
FILTER_TYPES = {
|
20
147
|
date: Filters::DateFilter,
|
21
148
|
datetime: Filters::DateTimeFilter,
|
22
149
|
string: Filters::StringFilter,
|
23
150
|
default: Filters::DefaultFilter,
|
24
|
-
xboolean: Filters::ExtendedBooleanFilter
|
25
|
-
boolean: Filters::BooleanFilter
|
151
|
+
xboolean: Filters::ExtendedBooleanFilter,
|
152
|
+
boolean: Filters::BooleanFilter,
|
26
153
|
integer: Filters::IntegerFilter,
|
27
154
|
enum: Filters::EnumFilter,
|
28
155
|
float: Filters::FloatFilter,
|
29
|
-
dynamic: Filters::DynamicFilter
|
30
|
-
}
|
31
|
-
|
32
|
-
DEFAULT_FILTER_BLOCK = Object.new
|
156
|
+
dynamic: Filters::DynamicFilter,
|
157
|
+
}.freeze
|
33
158
|
|
34
159
|
# @!visibility private
|
35
|
-
|
36
|
-
base.extend ClassMethods
|
37
|
-
base.class_eval do
|
160
|
+
DEFAULT_FILTER_BLOCK = Object.new
|
38
161
|
|
39
|
-
|
40
|
-
include Datagrid::Filters::CompositeFilters
|
41
|
-
class_attribute :filters_array, default: []
|
162
|
+
extend ActiveSupport::Concern
|
42
163
|
|
43
|
-
|
164
|
+
included do
|
165
|
+
include Datagrid::Core
|
166
|
+
class_attribute :filters_array, default: []
|
44
167
|
end
|
45
168
|
|
169
|
+
# Grid class methods related to filters
|
46
170
|
module ClassMethods
|
47
|
-
|
48
|
-
# Returns filter definition object by name
|
171
|
+
# @return [Datagrid::Filters::BaseFilter, nil] filter definition object by name
|
49
172
|
def filter_by_name(attribute)
|
50
173
|
if attribute.is_a?(Datagrid::Filters::BaseFilter)
|
51
174
|
unless ancestors.include?(attribute.grid_class)
|
52
|
-
raise "#{attribute.grid_class}##{attribute.name} filter doen't belong to #{self.class}"
|
175
|
+
raise ArgumentError, "#{attribute.grid_class}##{attribute.name} filter doen't belong to #{self.class}"
|
53
176
|
end
|
177
|
+
|
54
178
|
return attribute
|
55
179
|
end
|
56
|
-
|
180
|
+
filters.find do |filter|
|
57
181
|
filter.name == attribute.to_sym
|
58
182
|
end
|
59
183
|
end
|
@@ -62,61 +186,58 @@ module Datagrid
|
|
62
186
|
# This method automatically generates <tt>attr_accessor</tt> for filter name
|
63
187
|
# and adds it to the list of datagrid attributes.
|
64
188
|
#
|
65
|
-
#
|
66
|
-
#
|
67
|
-
#
|
68
|
-
#
|
69
|
-
#
|
70
|
-
#
|
71
|
-
#
|
72
|
-
#
|
73
|
-
#
|
74
|
-
#
|
75
|
-
#
|
76
|
-
#
|
77
|
-
#
|
78
|
-
#
|
79
|
-
#
|
80
|
-
#
|
81
|
-
#
|
82
|
-
#
|
83
|
-
#
|
84
|
-
#
|
85
|
-
#
|
86
|
-
#
|
87
|
-
#
|
88
|
-
#
|
89
|
-
#
|
90
|
-
#
|
91
|
-
#
|
92
|
-
# * <tt>:unless</tt> - specify the reverse condition when the filter can be dislayed and used.
|
93
|
-
# Accepts a block or a symbol with an instance method name
|
94
|
-
# * <tt>:input_options</tt> - options passed when rendering html input tag attributes.
|
95
|
-
# Use <tt>input_options.type</tt> to control input type including <tt>textarea</tt>.
|
96
|
-
# * <tt>:label_options</tt> - options passed when rendering html label tag attributes
|
97
|
-
#
|
98
|
-
# See: https://github.com/bogdan/datagrid/wiki/Filters for examples
|
189
|
+
# @param [Symbol] name filter name
|
190
|
+
# @param [Symbol] type filter type that defines type case and GUI representation of a filter
|
191
|
+
# @param [Hash] options hash of options
|
192
|
+
# @param [Proc] block proc to apply the filter
|
193
|
+
# @return [Datagrid::Filters::BaseFilter] Filter definition object
|
194
|
+
# @see Datagrid::Filters
|
195
|
+
# @option options [String] header Determines the header of the filter.
|
196
|
+
# @option options [Object, Proc] default The default filter value. Accepts a `Proc` to allow dynamic calculation.
|
197
|
+
# @option options [Boolean] range Whether the filter accepts two values to define a range.
|
198
|
+
# Supported by types: `:integer`, `:float`, `:date`, `:datetime`, and `:string`.
|
199
|
+
# @option options [Boolean, String] multiple If true, allows multiple values for the filter.
|
200
|
+
# Strings are parsed using a separator (default: `,`). Can accept a custom separator. Default: `false`.
|
201
|
+
# @option options [Boolean] allow_nil Whether the filter value can be `nil`. Default: `false`.
|
202
|
+
# @option options [Boolean] allow_blank Whether the filter value can be blank. Default: `false`.
|
203
|
+
# @option options [Symbol] before Specifies the position of this filter by placing it before another filter.
|
204
|
+
# Used with the `datagrid_form_for` helper.
|
205
|
+
# @option options [Symbol] after Specifies the position of this filter by placing it after another filter.
|
206
|
+
# Used with the `datagrid_form_for` helper.
|
207
|
+
# @option options [Boolean] dummy If true, the filter is not applied automatically and
|
208
|
+
# is only displayed in the form. Useful for manual application.
|
209
|
+
# @option options [Proc, Symbol] if Specifies a condition under which the filter is displayed and used.
|
210
|
+
# Accepts a block or the name of an instance method.
|
211
|
+
# @option options [Proc, Symbol] unless Specifies a condition under which the filter is NOT displayed or used.
|
212
|
+
# Accepts a block or the name of an instance method.
|
213
|
+
# @option options [Hash] input_options Options passed to the HTML input tag for rendering attributes.
|
214
|
+
# Use `input_options[:type]` to control the input type (e.g., `textarea`).
|
215
|
+
# @option options [Hash] label_options Options passed to the HTML label tag for rendering attributes.
|
99
216
|
def filter(name, type = :default, **options, &block)
|
100
217
|
klass = type.is_a?(Class) ? type : FILTER_TYPES[type]
|
101
218
|
raise ConfigurationError, "filter class #{type.inspect} not found" unless klass
|
102
219
|
|
103
220
|
position = Datagrid::Utils.extract_position_from_options(filters_array, options)
|
104
|
-
filter = klass.new(self, name, options, &block)
|
221
|
+
filter = klass.new(self, name, **options, &block)
|
105
222
|
filters_array.insert(position, filter)
|
106
223
|
|
107
224
|
datagrid_attribute(name) do |value|
|
108
225
|
filter.parse_values(value)
|
109
226
|
end
|
227
|
+
filter
|
110
228
|
end
|
111
229
|
|
230
|
+
# @!visibility private
|
112
231
|
def default_filter
|
113
232
|
DEFAULT_FILTER_BLOCK
|
114
233
|
end
|
115
234
|
|
235
|
+
# @!visibility private
|
116
236
|
def inspect
|
117
237
|
"#{super}(#{filters_inspection})"
|
118
238
|
end
|
119
239
|
|
240
|
+
# @return [Array<Datagrid::Filters::BaseFilter>] all defined filters
|
120
241
|
def filters
|
121
242
|
filters_array
|
122
243
|
end
|
@@ -124,26 +245,27 @@ module Datagrid
|
|
124
245
|
protected
|
125
246
|
|
126
247
|
def inherited(child_class)
|
127
|
-
super
|
128
|
-
child_class.filters_array =
|
248
|
+
super
|
249
|
+
child_class.filters_array = filters_array.clone
|
129
250
|
end
|
130
251
|
|
131
252
|
def filters_inspection
|
132
253
|
return "no filters" if filters.empty?
|
254
|
+
|
133
255
|
filters.map do |filter|
|
134
256
|
"#{filter.name}: #{filter.type}"
|
135
257
|
end.join(", ")
|
136
258
|
end
|
137
259
|
end
|
138
260
|
|
139
|
-
|
140
261
|
# @!visibility private
|
141
|
-
def initialize(
|
262
|
+
def initialize(...)
|
142
263
|
self.filters_array = self.class.filters_array.clone
|
143
|
-
|
144
|
-
|
264
|
+
filters_array.each do |filter|
|
265
|
+
value = filter.default(self)
|
266
|
+
self[filter.name] = value unless value.nil?
|
145
267
|
end
|
146
|
-
super
|
268
|
+
super
|
147
269
|
end
|
148
270
|
|
149
271
|
# @!visibility private
|
@@ -151,68 +273,71 @@ module Datagrid
|
|
151
273
|
apply_filters(super, filters)
|
152
274
|
end
|
153
275
|
|
154
|
-
#
|
276
|
+
# @return [Object] filter value for given filter definition
|
155
277
|
def filter_value(filter)
|
156
278
|
self[filter.name]
|
157
279
|
end
|
158
280
|
|
159
|
-
#
|
281
|
+
# @return [String] string representation of filter value
|
160
282
|
def filter_value_as_string(name)
|
161
283
|
filter = filter_by_name(name)
|
162
284
|
value = filter_value(filter)
|
163
285
|
if value.is_a?(Array)
|
164
|
-
value.map {|v| filter.format(v) }.join(filter.separator)
|
286
|
+
value.map { |v| filter.format(v) }.join(filter.separator)
|
165
287
|
else
|
166
288
|
filter.format(value)
|
167
289
|
end
|
168
290
|
end
|
169
291
|
|
170
|
-
#
|
292
|
+
# @return [Datagrid::Filters::BaseFilter, nil] filter object with the given name
|
171
293
|
def filter_by_name(name)
|
172
294
|
self.class.filter_by_name(name)
|
173
295
|
end
|
174
296
|
|
175
|
-
#
|
176
|
-
# Allows partial filtering
|
297
|
+
# @return [Array<Object>] assets filtered only by specified filters
|
177
298
|
def filter_by(*filters)
|
178
|
-
apply_filters(scope, filters.map{|f| filter_by_name(f)})
|
299
|
+
apply_filters(scope, filters.map { |f| filter_by_name(f) })
|
179
300
|
end
|
180
301
|
|
181
|
-
#
|
182
|
-
#
|
302
|
+
# @return [Array] the select options for the filter
|
303
|
+
# @raise [ArgumentError] if the filter doesn't support select options
|
183
304
|
def select_options(filter)
|
184
305
|
find_select_filter(filter).select(self)
|
185
306
|
end
|
186
307
|
|
187
|
-
#
|
308
|
+
# @return [void] sets all options as selected for a filter that has options
|
188
309
|
def select_all(filter)
|
189
310
|
filter = find_select_filter(filter)
|
190
311
|
self[filter.name] = select_values(filter)
|
191
312
|
end
|
192
313
|
|
193
|
-
#
|
314
|
+
# @return [Array] all possible values for the filter
|
194
315
|
def select_values(filter)
|
195
316
|
find_select_filter(filter).select_values(self)
|
196
317
|
end
|
197
318
|
|
198
|
-
|
199
|
-
self.class.default_filter
|
200
|
-
end
|
201
|
-
|
202
|
-
# Returns all currently enabled filters
|
319
|
+
# @return [Array<Datagrid::Filters::BaseFilter>] all currently enabled filters
|
203
320
|
def filters
|
204
321
|
self.class.filters.select do |filter|
|
205
322
|
filter.enabled?(self)
|
206
323
|
end
|
207
324
|
end
|
208
325
|
|
326
|
+
# @!visibility private
|
327
|
+
def default_filter
|
328
|
+
self.class.default_filter
|
329
|
+
end
|
330
|
+
|
209
331
|
protected
|
210
332
|
|
211
333
|
def find_select_filter(filter)
|
212
334
|
filter = filter_by_name(filter)
|
213
335
|
unless filter.class.included_modules.include?(::Datagrid::Filters::SelectOptions)
|
214
|
-
|
215
|
-
|
336
|
+
type = FILTER_TYPES.invert[filter.class].inspect
|
337
|
+
raise(
|
338
|
+
::Datagrid::ArgumentError,
|
339
|
+
"#{self.class.name}##{filter.name} with type #{type} can not have select options",
|
340
|
+
)
|
216
341
|
end
|
217
342
|
filter
|
218
343
|
end
|