datagrid 1.8.0 → 1.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/datagrid/core.rb CHANGED
@@ -1,26 +1,20 @@
1
1
  require "datagrid/drivers"
2
2
  require "active_support/core_ext/class/attribute"
3
+ require "active_model/attribute_assignment"
3
4
 
4
5
  module Datagrid
5
6
  module Core
7
+ include ::ActiveModel::AttributeAssignment
6
8
 
7
9
  # @!visibility private
8
10
  def self.included(base)
9
11
  base.extend ClassMethods
10
12
  base.class_eval do
11
13
  class_attribute :scope_value
12
-
13
- class_attribute :datagrid_attributes, instance_writer: false
14
- self.datagrid_attributes = []
15
-
14
+ class_attribute :datagrid_attributes, instance_writer: false, default: []
16
15
  class_attribute :dynamic_block, instance_writer: false
17
- class_attribute :forbidden_attributes_protection, instance_writer: false
18
- self.forbidden_attributes_protection = false
19
- if defined?(::ActiveModel::AttributeAssignment)
20
- include ::ActiveModel::AttributeAssignment
21
- end
16
+ class_attribute :forbidden_attributes_protection, instance_writer: false, default: false
22
17
  end
23
- base.include InstanceMethods
24
18
  end
25
19
 
26
20
  module ClassMethods
@@ -129,150 +123,155 @@ module Datagrid
129
123
 
130
124
  end
131
125
 
132
- module InstanceMethods
133
-
134
- def initialize(attributes = nil, &block)
135
- super()
136
-
137
- if attributes
138
- self.attributes = attributes
139
- end
126
+ def initialize(attributes = nil, &block)
127
+ super()
140
128
 
141
- instance_eval(&dynamic_block) if dynamic_block
142
- if block_given?
143
- self.scope(&block)
144
- end
129
+ if attributes
130
+ self.attributes = attributes
145
131
  end
146
132
 
147
- # @return [Hash<Symbol, Object>] grid attributes including filter values and ordering values
148
- def attributes
149
- result = {}
150
- self.datagrid_attributes.each do |name|
151
- result[name] = self[name]
152
- end
153
- result
133
+ instance_eval(&dynamic_block) if dynamic_block
134
+ if block_given?
135
+ self.scope(&block)
154
136
  end
137
+ end
155
138
 
156
- # @return [Object] Any datagrid attribute value
157
- def [](attribute)
158
- self.send(attribute)
159
- end
160
139
 
161
- # Assigns any datagrid attribute
162
- # @param attribute [Symbol, String] Datagrid attribute name
163
- # @param value [Object] Datagrid attribute value
164
- # @return [void]
165
- def []=(attribute, value)
166
- self.send(:"#{attribute}=", value)
140
+ # @return [Hash<Symbol, Object>] grid attributes including filter values and ordering values
141
+ def attributes
142
+ result = {}
143
+ self.datagrid_attributes.each do |name|
144
+ result[name] = self[name]
167
145
  end
146
+ result
147
+ end
168
148
 
169
- # @return [Object] a scope relation (e.g ActiveRecord::Relation) with all applied filters
170
- def assets
171
- scope
172
- end
149
+ # Updates datagrid attributes with a passed hash argument
150
+ # @param attributes [Hash<Symbol, Object>]
151
+ # @example
152
+ # grid = MyGrid.new
153
+ # grid.attributes = {first_name: 'John', last_name: 'Smith'}
154
+ # grid.first_name # => 'John'
155
+ # grid.last_name # => 'Smith'
156
+ def attributes=(attributes)
157
+ super(attributes)
158
+ end
173
159
 
174
- # Updates datagrid attributes with a passed hash argument
175
- def attributes=(attributes)
176
- if respond_to?(:assign_attributes)
177
- if !forbidden_attributes_protection && attributes.respond_to?(:permit!)
178
- attributes.permit!
179
- end
180
- assign_attributes(attributes)
181
- else
182
- attributes.each do |name, value|
183
- self[name] = value
184
- end
185
- self
186
- end
187
- end
160
+ # @return [Object] Any datagrid attribute value
161
+ def [](attribute)
162
+ self.public_send(attribute)
163
+ end
188
164
 
189
- # Returns serializable query arguments skipping all nil values
190
- # @example
191
- # grid = ProductsGrid.new(category: 'dresses', available: true)
192
- # grid.as_query # => {category: 'dresses', available: true}
193
- def as_query
194
- attributes = self.attributes.clone
195
- attributes.each do |key, value|
196
- attributes.delete(key) if value.nil?
197
- end
198
- attributes
199
- end
165
+ # Assigns any datagrid attribute
166
+ # @param attribute [Symbol, String] Datagrid attribute name
167
+ # @param value [Object] Datagrid attribute value
168
+ # @return [void]
169
+ def []=(attribute, value)
170
+ self.public_send(:"#{attribute}=", value)
171
+ end
200
172
 
201
- # @return [Hash<Symbol, Hash<Symbol, Object>>] query parameters to link this grid from a page
202
- # @example
203
- # grid = ProductsGrid.new(category: 'dresses', available: true)
204
- # Rails.application.routes.url_helpers.products_path(grid.query_params)
205
- # # => "/products?products_grid[category]=dresses&products_grid[available]=true"
206
- def query_params(attributes = {})
207
- { param_name.to_sym => as_query.merge(attributes) }
208
- end
173
+ # @return [Object] a scope relation (e.g ActiveRecord::Relation) with all applied filters
174
+ def assets
175
+ scope
176
+ end
209
177
 
210
- # Redefines scope at instance level
211
- # @example
212
- # class MyGrid
213
- # scope { Article.order('created_at desc') }
214
- # end
215
- #
216
- # grid = MyGrid.new
217
- # grid.scope do |scope|
218
- # scope.where(author_id: current_user.id)
219
- # end
220
- # grid.assets
221
- # # => SELECT * FROM articles WHERE author_id = ?
222
- # # ORDER BY created_at desc
223
- def scope(&block)
224
- if block_given?
225
- current_scope = scope
226
- self.scope_value = proc {
227
- Datagrid::Utils.apply_args(current_scope, &block)
228
- }
229
- self
230
- else
231
- scope = original_scope
232
- driver.to_scope(scope)
233
- end
178
+ # Returns serializable query arguments skipping all nil values
179
+ # @example
180
+ # grid = ProductsGrid.new(category: 'dresses', available: true)
181
+ # grid.as_query # => {category: 'dresses', available: true}
182
+ def as_query
183
+ attributes = self.attributes.clone
184
+ attributes.each do |key, value|
185
+ attributes.delete(key) if value.nil?
234
186
  end
187
+ attributes
188
+ end
235
189
 
236
- # @!visibility private
237
- def original_scope
238
- check_scope_defined!
239
- scope_value.call
240
- end
190
+ # @return [Hash<Symbol, Hash<Symbol, Object>>] query parameters to link this grid from a page
191
+ # @example
192
+ # grid = ProductsGrid.new(category: 'dresses', available: true)
193
+ # Rails.application.routes.url_helpers.products_path(grid.query_params)
194
+ # # => "/products?products_grid[category]=dresses&products_grid[available]=true"
195
+ def query_params(attributes = {})
196
+ { param_name.to_sym => as_query.merge(attributes) }
197
+ end
241
198
 
242
- # Resets current instance scope to default scope defined in a class
243
- # @return [void]
244
- def reset_scope
245
- self.scope_value = self.class.scope_value
199
+ # Redefines scope at instance level
200
+ # @example
201
+ # class MyGrid
202
+ # scope { Article.order('created_at desc') }
203
+ # end
204
+ #
205
+ # grid = MyGrid.new
206
+ # grid.scope do |scope|
207
+ # scope.where(author_id: current_user.id)
208
+ # end
209
+ # grid.assets
210
+ # # => SELECT * FROM articles WHERE author_id = ?
211
+ # # ORDER BY created_at desc
212
+ def scope(&block)
213
+ if block_given?
214
+ current_scope = scope
215
+ self.scope_value = proc {
216
+ Datagrid::Utils.apply_args(current_scope, &block)
217
+ }
218
+ self
219
+ else
220
+ scope = original_scope
221
+ driver.to_scope(scope)
246
222
  end
223
+ end
247
224
 
248
- # @return [Boolean] true if the scope was redefined for this instance of grid object
249
- def redefined_scope?
250
- self.class.scope_value != scope_value
251
- end
225
+ # @!visibility private
226
+ def original_scope
227
+ check_scope_defined!
228
+ scope_value.call
229
+ end
252
230
 
253
- # @!visibility private
254
- def driver
255
- self.class.driver
256
- end
231
+ # Resets current instance scope to default scope defined in a class
232
+ # @return [void]
233
+ def reset_scope
234
+ self.scope_value = self.class.scope_value
235
+ end
257
236
 
258
- # @!visibility private
259
- def check_scope_defined!(message = nil)
260
- self.class.send :check_scope_defined!, message
261
- end
237
+ # @return [Boolean] true if the scope was redefined for this instance of grid object
238
+ def redefined_scope?
239
+ self.class.scope_value != scope_value
240
+ end
262
241
 
263
- # @return [String] a datagrid attributes and their values in inspection form
264
- def inspect
265
- attrs = attributes.map do |key, value|
266
- "#{key}: #{value.inspect}"
267
- end.join(", ")
268
- "#<#{self.class} #{attrs}>"
269
- end
242
+ # @!visibility private
243
+ def driver
244
+ self.class.driver
245
+ end
270
246
 
271
- def ==(other)
272
- self.class == other.class &&
273
- attributes == other.attributes &&
274
- scope == other.scope
275
- end
247
+ # @!visibility private
248
+ def check_scope_defined!(message = nil)
249
+ self.class.send :check_scope_defined!, message
250
+ end
251
+
252
+ # @return [String] a datagrid attributes and their values in inspection form
253
+ def inspect
254
+ attrs = attributes.map do |key, value|
255
+ "#{key}: #{value.inspect}"
256
+ end.join(", ")
257
+ "#<#{self.class} #{attrs}>"
258
+ end
259
+
260
+ def ==(other)
261
+ self.class == other.class &&
262
+ attributes == other.attributes &&
263
+ scope == other.scope
264
+ end
265
+
266
+ # Resets loaded assets and column values cache
267
+ # @return [void]
268
+ def reset
269
+ assets.reset
270
+ end
271
+
272
+ protected
273
+ def sanitize_for_mass_assignment(attributes)
274
+ forbidden_attributes_protection ? super(attributes) : attributes
276
275
  end
277
276
  end
278
277
  end
@@ -5,11 +5,10 @@ module Datagrid
5
5
 
6
6
  TIMESTAMP_CLASSES = [DateTime, Time, ActiveSupport::TimeWithZone]
7
7
 
8
- class_attribute :subclasses
8
+ class_attribute :subclasses, default: []
9
9
 
10
10
  def self.inherited(base)
11
11
  super(base)
12
- self.subclasses ||= []
13
12
  self.subclasses << base
14
13
  end
15
14
 
@@ -4,7 +4,10 @@ module Datagrid
4
4
  class Array < AbstractDriver
5
5
 
6
6
  def self.match?(scope)
7
- !Datagrid::Drivers::ActiveRecord.match?(scope) && (scope.is_a?(::Array) || scope.is_a?(Enumerator))
7
+ !Datagrid::Drivers::ActiveRecord.match?(scope) && (
8
+ scope.is_a?(::Array) || scope.is_a?(Enumerator) ||
9
+ (defined?(::ActiveRecord::Result) && scope.is_a?(::ActiveRecord::Result))
10
+ )
8
11
  end
9
12
 
10
13
  def to_scope(scope)
@@ -13,7 +16,7 @@ module Datagrid
13
16
 
14
17
  def where(scope, attribute, value)
15
18
  scope.select do |object|
16
- object.send(attribute) == value
19
+ object.public_send(attribute) == value
17
20
  end
18
21
  end
19
22
 
@@ -21,7 +24,7 @@ module Datagrid
21
24
  return scope unless order
22
25
  return scope if order.empty?
23
26
  scope.sort_by do |object|
24
- object.send(order)
27
+ object.public_send(order)
25
28
  end
26
29
  end
27
30
 
@@ -39,15 +42,13 @@ module Datagrid
39
42
 
40
43
  def greater_equal(scope, field, value)
41
44
  scope.select do |object|
42
- compare_value = object.send(field)
43
- compare_value.respond_to?(:>=) && compare_value >= value
45
+ object.public_send(field) >= value
44
46
  end
45
47
  end
46
48
 
47
49
  def less_equal(scope, field, value)
48
50
  scope.select do |object|
49
- compare_value = object.send(field)
50
- compare_value.respond_to?(:<=) && compare_value <= value
51
+ object.public_send(field) <= value
51
52
  end
52
53
  end
53
54
 
@@ -57,12 +58,12 @@ module Datagrid
57
58
 
58
59
  def is_timestamp?(scope, column_name)
59
60
  has_column?(scope, column_name) &&
60
- timestamp_class?(scope.first.send(column_name).class)
61
+ timestamp_class?(scope.first.public_send(column_name).class)
61
62
  end
62
63
 
63
64
  def contains(scope, field, value)
64
65
  scope.select do |object|
65
- object.send(field).to_s.include?(value)
66
+ object.public_send(field).to_s.include?(value)
66
67
  end
67
68
  end
68
69
 
@@ -60,7 +60,7 @@ module Datagrid
60
60
  end
61
61
 
62
62
  def normalized_column_type(scope, field)
63
- type = to_scope(scope).klass.fields[field.to_s].try(:type)
63
+ type = to_scope(scope).klass.fields[field.to_s]&.type
64
64
  return nil unless type
65
65
  {
66
66
  [BigDecimal , String, Symbol, Range, Array, Hash, ] => :string,
@@ -155,7 +155,7 @@ class Datagrid::Filters::BaseFilter
155
155
  when String
156
156
  value.split(separator)
157
157
  when Range
158
- [value.first, value.last]
158
+ [value.begin, value.end]
159
159
  when Array
160
160
  value
161
161
  else
@@ -173,8 +173,8 @@ class Datagrid::Filters::BaseFilter
173
173
 
174
174
  def default_filter(value, scope, grid)
175
175
  return nil if dummy?
176
- if !driver.has_column?(scope, name) && scope.respond_to?(name)
177
- scope.send(name, value)
176
+ if !driver.has_column?(scope, name) && scope.respond_to?(name, true)
177
+ scope.public_send(name, value)
178
178
  else
179
179
  default_filter_where(scope, value)
180
180
  end
@@ -6,7 +6,7 @@ class Datagrid::Filters::DateFilter < Datagrid::Filters::BaseFilter
6
6
 
7
7
  def apply(grid_object, scope, value)
8
8
  if value.is_a?(Range)
9
- value = value.first.beginning_of_day..value.last.end_of_day
9
+ value = value.begin&.beginning_of_day..value.end&.end_of_day
10
10
  end
11
11
  super(grid_object, scope, value)
12
12
  end
@@ -3,6 +3,8 @@ class Datagrid::Filters::ExtendedBooleanFilter < Datagrid::Filters::EnumFilter
3
3
 
4
4
  YES = "YES"
5
5
  NO = "NO"
6
+ TRUTH_VALUES = [true, 'true', "y", "yes"]
7
+ FALSE_VALUES = [false, 'false', "n", "no"]
6
8
 
7
9
  def initialize(report, attribute, options = {}, &block)
8
10
  options[:select] = -> { boolean_select }
@@ -15,10 +17,11 @@ class Datagrid::Filters::ExtendedBooleanFilter < Datagrid::Filters::EnumFilter
15
17
  end
16
18
 
17
19
  def parse(value)
18
- case
19
- when value == true
20
+ value = value.downcase if value.is_a?(String)
21
+ case value
22
+ when *TRUTH_VALUES
20
23
  YES
21
- when value == false
24
+ when *FALSE_VALUES
22
25
  NO
23
26
  when value.blank?
24
27
  nil
@@ -38,11 +38,9 @@ module Datagrid
38
38
 
39
39
  include Datagrid::Core
40
40
  include Datagrid::Filters::CompositeFilters
41
- class_attribute :filters_array
42
- self.filters_array = []
41
+ class_attribute :filters_array, default: []
43
42
 
44
43
  end
45
- base.include InstanceMethods
46
44
  end
47
45
 
48
46
  module ClassMethods
@@ -138,94 +136,91 @@ module Datagrid
138
136
  end
139
137
  end
140
138
 
141
- module InstanceMethods
142
139
 
143
- # @!visibility private
144
- def initialize(*args, &block)
145
- self.filters_array = self.class.filters_array.clone
146
- self.filters_array.each do |filter|
147
- self[filter.name] = filter.default(self)
148
- end
149
- super(*args, &block)
140
+ # @!visibility private
141
+ def initialize(*args, &block)
142
+ self.filters_array = self.class.filters_array.clone
143
+ self.filters_array.each do |filter|
144
+ self[filter.name] = filter.default(self)
150
145
  end
146
+ super(*args, &block)
147
+ end
151
148
 
152
- # @!visibility private
153
- def assets
154
- apply_filters(super, filters)
155
- end
149
+ # @!visibility private
150
+ def assets
151
+ apply_filters(super, filters)
152
+ end
156
153
 
157
- # Returns filter value for given filter definition
158
- def filter_value(filter)
159
- self[filter.name]
160
- end
154
+ # Returns filter value for given filter definition
155
+ def filter_value(filter)
156
+ self[filter.name]
157
+ end
161
158
 
162
- # Returns string representation of filter value
163
- def filter_value_as_string(name)
164
- filter = filter_by_name(name)
165
- value = filter_value(filter)
166
- if value.is_a?(Array)
167
- value.map {|v| filter.format(v) }.join(filter.separator)
168
- else
169
- filter.format(value)
170
- end
159
+ # Returns string representation of filter value
160
+ def filter_value_as_string(name)
161
+ filter = filter_by_name(name)
162
+ value = filter_value(filter)
163
+ if value.is_a?(Array)
164
+ value.map {|v| filter.format(v) }.join(filter.separator)
165
+ else
166
+ filter.format(value)
171
167
  end
168
+ end
172
169
 
173
- # Returns filter object with the given name
174
- def filter_by_name(name)
175
- self.class.filter_by_name(name)
176
- end
170
+ # Returns filter object with the given name
171
+ def filter_by_name(name)
172
+ self.class.filter_by_name(name)
173
+ end
177
174
 
178
- # Returns assets filtered only by specified filters
179
- # Allows partial filtering
180
- def filter_by(*filters)
181
- apply_filters(scope, filters.map{|f| filter_by_name(f)})
182
- end
175
+ # Returns assets filtered only by specified filters
176
+ # Allows partial filtering
177
+ def filter_by(*filters)
178
+ apply_filters(scope, filters.map{|f| filter_by_name(f)})
179
+ end
183
180
 
184
- # Returns select options for specific filter or filter name
185
- # If given filter doesn't support select options raises `ArgumentError`
186
- def select_options(filter)
187
- find_select_filter(filter).select(self)
188
- end
181
+ # Returns select options for specific filter or filter name
182
+ # If given filter doesn't support select options raises `ArgumentError`
183
+ def select_options(filter)
184
+ find_select_filter(filter).select(self)
185
+ end
189
186
 
190
- # Sets all options as selected for a filter that has options
191
- def select_all(filter)
192
- filter = find_select_filter(filter)
193
- self[filter.name] = select_values(filter)
194
- end
187
+ # Sets all options as selected for a filter that has options
188
+ def select_all(filter)
189
+ filter = find_select_filter(filter)
190
+ self[filter.name] = select_values(filter)
191
+ end
195
192
 
196
- # Returns all values that can be set to a filter with select options
197
- def select_values(filter)
198
- find_select_filter(filter).select_values(self)
199
- end
193
+ # Returns all values that can be set to a filter with select options
194
+ def select_values(filter)
195
+ find_select_filter(filter).select_values(self)
196
+ end
200
197
 
201
- def default_filter
202
- self.class.default_filter
203
- end
198
+ def default_filter
199
+ self.class.default_filter
200
+ end
204
201
 
205
- # Returns all currently enabled filters
206
- def filters
207
- self.class.filters.select do |filter|
208
- filter.enabled?(self)
209
- end
202
+ # Returns all currently enabled filters
203
+ def filters
204
+ self.class.filters.select do |filter|
205
+ filter.enabled?(self)
210
206
  end
207
+ end
211
208
 
212
- protected
209
+ protected
213
210
 
214
- def find_select_filter(filter)
215
- filter = filter_by_name(filter)
216
- unless filter.class.included_modules.include?(::Datagrid::Filters::SelectOptions)
217
- raise ::Datagrid::ArgumentError,
218
- "#{self.class.name}##{filter.name} with type #{FILTER_TYPES.invert[filter.class].inspect} can not have select options"
219
- end
220
- filter
211
+ def find_select_filter(filter)
212
+ filter = filter_by_name(filter)
213
+ 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"
221
216
  end
217
+ filter
218
+ end
222
219
 
223
- def apply_filters(current_scope, filters)
224
- filters.inject(current_scope) do |result, filter|
225
- filter.apply(self, result, filter_value(filter))
226
- end
220
+ def apply_filters(current_scope, filters)
221
+ filters.inject(current_scope) do |result, filter|
222
+ filter.apply(self, result, filter_value(filter))
227
223
  end
228
224
  end
229
-
230
225
  end
231
226
  end
@@ -10,12 +10,8 @@ module Datagrid
10
10
  # * <tt>text_field</tt> for other filter types
11
11
  def datagrid_filter(filter_or_attribute, partials: nil, **options, &block)
12
12
  filter = datagrid_get_filter(filter_or_attribute)
13
- self.send(
14
- filter.form_builder_helper_name, filter,
15
- **filter.input_options,
16
- **add_html_classes(options, filter.name, datagrid_filter_html_class(filter)),
17
- &block
18
- )
13
+ options = add_html_classes({**filter.input_options, **options}, filter.name, datagrid_filter_html_class(filter))
14
+ self.send( filter.form_builder_helper_name, filter, **options, &block)
19
15
  end
20
16
 
21
17
  # @param filter_or_attribute [Datagrid::Filters::BaseFilter, String, Symbol] filter object or filter name
@@ -93,7 +89,7 @@ module Datagrid
93
89
  end
94
90
 
95
91
  def enum_checkbox_checked?(filter, option_value)
96
- current_value = object.send(filter.name)
92
+ current_value = object.public_send(filter.name)
97
93
  if current_value.respond_to?(:include?)
98
94
  # Typecast everything to string
99
95
  # to remove difference between String and Symbol
@@ -170,7 +166,7 @@ module Datagrid
170
166
  def datagrid_range_filter_options(object, filter, type, options)
171
167
  type_method_map = {from: :first, to: :last}
172
168
  options = add_html_classes(options, type)
173
- options[:value] = filter.format(object[filter.name].try(type_method_map[type]))
169
+ options[:value] = filter.format(object[filter.name]&.public_send(type_method_map[type]))
174
170
  # In case of datagrid ranged filter
175
171
  # from and to input will have same id
176
172
  if !options.key?(:id)