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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +2 -0
  3. data/CHANGELOG.md +11 -1
  4. data/{Readme.markdown → README.md} +44 -29
  5. data/app/assets/stylesheets/datagrid.css +145 -0
  6. data/app/views/datagrid/_enum_checkboxes.html.erb +5 -3
  7. data/app/views/datagrid/_form.html.erb +4 -4
  8. data/app/views/datagrid/_head.html.erb +26 -3
  9. data/app/views/datagrid/_range_filter.html.erb +5 -3
  10. data/app/views/datagrid/_row.html.erb +12 -1
  11. data/app/views/datagrid/_table.html.erb +4 -4
  12. data/datagrid.gemspec +8 -7
  13. data/lib/datagrid/active_model.rb +9 -17
  14. data/lib/datagrid/base.rb +39 -0
  15. data/lib/datagrid/column_names_attribute.rb +9 -11
  16. data/lib/datagrid/columns/column.rb +155 -133
  17. data/lib/datagrid/columns.rb +325 -115
  18. data/lib/datagrid/configuration.rb +33 -4
  19. data/lib/datagrid/core.rb +89 -54
  20. data/lib/datagrid/deprecated_object.rb +20 -0
  21. data/lib/datagrid/drivers/abstract_driver.rb +12 -23
  22. data/lib/datagrid/drivers/active_record.rb +24 -26
  23. data/lib/datagrid/drivers/array.rb +22 -14
  24. data/lib/datagrid/drivers/mongo_mapper.rb +15 -14
  25. data/lib/datagrid/drivers/mongoid.rb +15 -17
  26. data/lib/datagrid/drivers/sequel.rb +14 -19
  27. data/lib/datagrid/drivers.rb +2 -1
  28. data/lib/datagrid/engine.rb +11 -3
  29. data/lib/datagrid/filters/base_filter.rb +166 -143
  30. data/lib/datagrid/filters/boolean_filter.rb +19 -5
  31. data/lib/datagrid/filters/date_filter.rb +33 -35
  32. data/lib/datagrid/filters/date_time_filter.rb +24 -16
  33. data/lib/datagrid/filters/default_filter.rb +9 -3
  34. data/lib/datagrid/filters/dynamic_filter.rb +151 -105
  35. data/lib/datagrid/filters/enum_filter.rb +43 -19
  36. data/lib/datagrid/filters/extended_boolean_filter.rb +39 -31
  37. data/lib/datagrid/filters/float_filter.rb +15 -5
  38. data/lib/datagrid/filters/integer_filter.rb +21 -10
  39. data/lib/datagrid/filters/ranged_filter.rb +66 -45
  40. data/lib/datagrid/filters/select_options.rb +58 -49
  41. data/lib/datagrid/filters/string_filter.rb +9 -4
  42. data/lib/datagrid/filters.rb +204 -79
  43. data/lib/datagrid/form_builder.rb +116 -128
  44. data/lib/datagrid/generators/scaffold.rb +184 -0
  45. data/lib/datagrid/generators/views.rb +20 -0
  46. data/lib/datagrid/helper.rb +436 -69
  47. data/lib/datagrid/ordering.rb +26 -29
  48. data/lib/datagrid/rspec.rb +6 -10
  49. data/lib/datagrid/utils.rb +37 -30
  50. data/lib/datagrid/version.rb +3 -1
  51. data/lib/datagrid.rb +8 -28
  52. data/templates/base.rb.erb +6 -4
  53. data/templates/grid.rb.erb +1 -1
  54. metadata +17 -17
  55. data/app/assets/stylesheets/datagrid.sass +0 -134
  56. data/lib/datagrid/filters/composite_filters.rb +0 -49
  57. data/lib/datagrid/renderer.rb +0 -157
  58. data/lib/datagrid/scaffold.rb +0 -129
  59. data/lib/tasks/datagrid_tasks.rake +0 -15
  60. data/templates/controller.rb.erb +0 -6
  61. data/templates/index.html.erb +0 -5
@@ -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
- def self.included(base)
36
- base.extend ClassMethods
37
- base.class_eval do
160
+ DEFAULT_FILTER_BLOCK = Object.new
38
161
 
39
- include Datagrid::Core
40
- include Datagrid::Filters::CompositeFilters
41
- class_attribute :filters_array, default: []
162
+ extend ActiveSupport::Concern
42
163
 
43
- end
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
- self.filters.find do |filter|
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
- # Arguments:
66
- #
67
- # * <tt>name</tt> - filter name
68
- # * <tt>type</tt> - filter type that defines type case and GUI representation of a filter
69
- # * <tt>options</tt> - hash of options
70
- # * <tt>block</tt> - proc to apply the filter
71
- #
72
- # Available options:
73
- #
74
- # * <tt>:header</tt> - determines the header of the filter
75
- # * <tt>:default</tt> - the default filter value. Able to accept a <tt>Proc</tt> in case default should be recalculated
76
- # * <tt>:range</tt> - if true, filter can accept two values that are treated as a range that will be used for filtering
77
- # Not all of the filter types support this option. Here are the list of types that do:
78
- # <tt>:integer</tt>, <tt>:float</tt>, <tt>:date</tt>, <tt>:datetime</tt>, <tt>:string</tt>
79
- # * <tt>:multiple</tt> - if true multiple values can be assigned to this filter.
80
- # If String is assigned as a filter value, it is parsed from string using a separator symbol (`,` by default).
81
- # But you can specify a different separator as option value. Default: false.
82
- # * <tt>:allow_nil</tt> - determines if the value can be nil
83
- # * <tt>:allow_blank</tt> - determines if the value can be blank
84
- # * <tt>:before</tt> - determines the position of this filter,
85
- # by adding it before the filter passed here (when using datagrid_form_for helper)
86
- # * <tt>:after</tt> - determines the position of this filter,
87
- # by adding it after the filter passed here (when using datagrid_form_for helper)
88
- # * <tt>:dummy</tt> - if true, this filter will not be applied automatically
89
- # and will be just displayed in form. In case you may want to apply it manually.
90
- # * <tt>:if</tt> - specify the condition when the filter can be dislayed and used.
91
- # Accepts a block or a symbol with an instance method name
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(child_class)
128
- child_class.filters_array = self.filters_array.clone
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(*args, &block)
262
+ def initialize(...)
142
263
  self.filters_array = self.class.filters_array.clone
143
- self.filters_array.each do |filter|
144
- self[filter.name] = filter.default(self)
264
+ filters_array.each do |filter|
265
+ value = filter.default(self)
266
+ self[filter.name] = value unless value.nil?
145
267
  end
146
- super(*args, &block)
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
- # Returns filter value for given filter definition
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
- # Returns string representation of filter value
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
- # Returns filter object with the given name
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
- # Returns assets filtered only by specified filters
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
- # Returns select options for specific filter or filter name
182
- # If given filter doesn't support select options raises `ArgumentError`
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
- # Sets all options as selected for a filter that has options
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
- # Returns all values that can be set to a filter with select options
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
- def default_filter
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
- raise ::Datagrid::ArgumentError,
215
- "#{self.class.name}##{filter.name} with type #{FILTER_TYPES.invert[filter.class].inspect} can not have select options"
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