datagrid 1.6.3 → 1.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +19 -1
- data/Readme.markdown +11 -11
- data/app/views/datagrid/_enum_checkboxes.html.erb +2 -2
- data/app/views/datagrid/_form.html.erb +2 -3
- data/app/views/datagrid/_order_for.html.erb +2 -2
- data/app/views/datagrid/_range_filter.html.erb +3 -3
- data/datagrid.gemspec +10 -14
- data/lib/datagrid/active_model.rb +25 -29
- data/lib/datagrid/column_names_attribute.rb +11 -9
- data/lib/datagrid/columns/column.rb +10 -8
- data/lib/datagrid/columns.rb +142 -176
- data/lib/datagrid/configuration.rb +6 -1
- data/lib/datagrid/core.rb +63 -30
- data/lib/datagrid/drivers/abstract_driver.rb +2 -1
- data/lib/datagrid/drivers/active_record.rb +4 -3
- data/lib/datagrid/drivers/array.rb +2 -1
- data/lib/datagrid/drivers/mongo_mapper.rb +2 -1
- data/lib/datagrid/drivers/mongoid.rb +3 -2
- data/lib/datagrid/drivers/sequel.rb +8 -3
- data/lib/datagrid/drivers.rb +2 -1
- data/lib/datagrid/engine.rb +3 -2
- data/lib/datagrid/filters/base_filter.rb +8 -4
- data/lib/datagrid/filters/boolean_filter.rb +2 -1
- data/lib/datagrid/filters/composite_filters.rb +8 -12
- data/lib/datagrid/filters/dynamic_filter.rb +1 -1
- data/lib/datagrid/filters/extended_boolean_filter.rb +2 -1
- data/lib/datagrid/filters/select_options.rb +34 -1
- data/lib/datagrid/filters.rb +52 -30
- data/lib/datagrid/form_builder.rb +53 -35
- data/lib/datagrid/helper.rb +43 -61
- data/lib/datagrid/locale/en.yml +5 -1
- data/lib/datagrid/ordering.rb +15 -15
- data/lib/datagrid/renderer.rb +71 -15
- data/lib/datagrid/rspec.rb +4 -4
- data/lib/datagrid/scaffold.rb +1 -1
- data/lib/datagrid/utils.rb +1 -0
- data/lib/datagrid/version.rb +1 -1
- data/lib/datagrid.rb +2 -1
- data/templates/grid.rb.erb +1 -1
- data/templates/index.html.erb +1 -1
- metadata +8 -9
- data/lib/datagrid/filters/boolean_enum_filter.rb +0 -17
data/lib/datagrid/core.rb
CHANGED
@@ -4,27 +4,29 @@ require "active_support/core_ext/class/attribute"
|
|
4
4
|
module Datagrid
|
5
5
|
module Core
|
6
6
|
|
7
|
+
# @!visibility private
|
7
8
|
def self.included(base)
|
8
|
-
base.extend
|
9
|
+
base.extend ClassMethods
|
9
10
|
base.class_eval do
|
10
11
|
class_attribute :scope_value
|
11
12
|
|
12
13
|
class_attribute :datagrid_attributes, instance_writer: false
|
13
14
|
self.datagrid_attributes = []
|
14
15
|
|
15
|
-
class_attribute :dynamic_block, :
|
16
|
+
class_attribute :dynamic_block, instance_writer: false
|
16
17
|
class_attribute :forbidden_attributes_protection, instance_writer: false
|
17
18
|
self.forbidden_attributes_protection = false
|
18
19
|
if defined?(::ActiveModel::AttributeAssignment)
|
19
20
|
include ::ActiveModel::AttributeAssignment
|
20
21
|
end
|
21
22
|
end
|
22
|
-
base.
|
23
|
-
end
|
23
|
+
base.include InstanceMethods
|
24
|
+
end
|
24
25
|
|
25
26
|
module ClassMethods
|
26
27
|
|
27
|
-
|
28
|
+
# @!visibility private
|
29
|
+
def datagrid_attribute(name, &block)
|
28
30
|
unless datagrid_attributes.include?(name)
|
29
31
|
block ||= lambda do |value|
|
30
32
|
value
|
@@ -41,6 +43,11 @@ module Datagrid
|
|
41
43
|
end
|
42
44
|
|
43
45
|
# Defines a scope at class level
|
46
|
+
# @return [void]
|
47
|
+
# @example
|
48
|
+
# scope { User }
|
49
|
+
# scope { Project.where(deleted: false) }
|
50
|
+
# scope { Project.preload(:stages) }
|
44
51
|
def scope(&block)
|
45
52
|
if block
|
46
53
|
current_scope = scope_value
|
@@ -49,17 +56,28 @@ module Datagrid
|
|
49
56
|
}
|
50
57
|
self
|
51
58
|
else
|
52
|
-
|
53
|
-
|
59
|
+
scope = original_scope
|
60
|
+
driver.to_scope(scope)
|
54
61
|
end
|
55
62
|
end
|
56
63
|
|
57
|
-
|
58
|
-
|
64
|
+
# @!visibility private
|
65
|
+
def original_scope
|
66
|
+
check_scope_defined!
|
67
|
+
scope_value.call
|
68
|
+
end
|
69
|
+
|
70
|
+
# @!visibility private
|
71
|
+
def driver
|
72
|
+
@driver ||= Drivers::AbstractDriver.guess_driver(scope_value.call).new
|
59
73
|
end
|
60
74
|
|
61
75
|
# Allows dynamic columns definition, that could not be defined at class level
|
62
|
-
#
|
76
|
+
# Columns that depend on the database state or third party service
|
77
|
+
# can be defined this way.
|
78
|
+
# @param block [Proc] block that defines dynamic columns
|
79
|
+
# @return [void]
|
80
|
+
# @example
|
63
81
|
# class MerchantsGrid
|
64
82
|
#
|
65
83
|
# scope { Merchant }
|
@@ -69,12 +87,15 @@ module Datagrid
|
|
69
87
|
# dynamic do
|
70
88
|
# PurchaseCategory.all.each do |category|
|
71
89
|
# column(:"#{category.name.underscore}_sales") do |merchant|
|
72
|
-
# merchant.purchases.where(:
|
90
|
+
# merchant.purchases.where(category_id: category.id).count
|
73
91
|
# end
|
74
92
|
# end
|
75
93
|
# end
|
76
94
|
# end
|
77
95
|
#
|
96
|
+
# ProductCategory.create!(name: 'Swimwear')
|
97
|
+
# ProductCategory.create!(name: 'Sportswear')
|
98
|
+
#
|
78
99
|
# grid = MerchantsGrid.new
|
79
100
|
# grid.data # => [
|
80
101
|
# # [ "Name", "Swimwear Sales", "Sportswear Sales", ... ]
|
@@ -95,6 +116,7 @@ module Datagrid
|
|
95
116
|
end
|
96
117
|
|
97
118
|
protected
|
119
|
+
|
98
120
|
def check_scope_defined!(message = nil)
|
99
121
|
message ||= "#{self}.scope is not defined"
|
100
122
|
raise(Datagrid::ConfigurationError, message) unless scope_value
|
@@ -105,7 +127,7 @@ module Datagrid
|
|
105
127
|
child_class.datagrid_attributes = self.datagrid_attributes.clone
|
106
128
|
end
|
107
129
|
|
108
|
-
end
|
130
|
+
end
|
109
131
|
|
110
132
|
module InstanceMethods
|
111
133
|
|
@@ -122,8 +144,7 @@ module Datagrid
|
|
122
144
|
end
|
123
145
|
end
|
124
146
|
|
125
|
-
#
|
126
|
-
# and ordering values
|
147
|
+
# @return [Hash<Symbol, Object>] grid attributes including filter values and ordering values
|
127
148
|
def attributes
|
128
149
|
result = {}
|
129
150
|
self.datagrid_attributes.each do |name|
|
@@ -132,21 +153,24 @@ module Datagrid
|
|
132
153
|
result
|
133
154
|
end
|
134
155
|
|
135
|
-
#
|
156
|
+
# @return [Object] Any datagrid attribute value
|
136
157
|
def [](attribute)
|
137
158
|
self.send(attribute)
|
138
159
|
end
|
139
160
|
|
161
|
+
# Assigns any datagrid attribute
|
162
|
+
# @param attribute [Symbol, String] Datagrid attribute name
|
163
|
+
# @param value [Object] Datagrid attribute value
|
164
|
+
# @return [void]
|
140
165
|
def []=(attribute, value)
|
141
166
|
self.send(:"#{attribute}=", value)
|
142
167
|
end
|
143
168
|
|
144
|
-
#
|
169
|
+
# @return [Object] a scope relation (e.g ActiveRecord::Relation) with all applied filters
|
145
170
|
def assets
|
146
|
-
|
171
|
+
scope
|
147
172
|
end
|
148
173
|
|
149
|
-
|
150
174
|
# Updates datagrid attributes with a passed hash argument
|
151
175
|
def attributes=(attributes)
|
152
176
|
if respond_to?(:assign_attributes)
|
@@ -163,7 +187,7 @@ module Datagrid
|
|
163
187
|
end
|
164
188
|
|
165
189
|
# Returns serializable query arguments skipping all nil values
|
166
|
-
#
|
190
|
+
# @example
|
167
191
|
# grid = ProductsGrid.new(category: 'dresses', available: true)
|
168
192
|
# grid.as_query # => {category: 'dresses', available: true}
|
169
193
|
def as_query
|
@@ -174,8 +198,8 @@ module Datagrid
|
|
174
198
|
attributes
|
175
199
|
end
|
176
200
|
|
177
|
-
#
|
178
|
-
#
|
201
|
+
# @return [Hash<Symbol, Hash<Symbol, Object>>] query parameters to link this grid from a page
|
202
|
+
# @example
|
179
203
|
# grid = ProductsGrid.new(category: 'dresses', available: true)
|
180
204
|
# Rails.application.routes.url_helpers.products_path(grid.query_params)
|
181
205
|
# # => "/products?products_grid[category]=dresses&products_grid[available]=true"
|
@@ -184,19 +208,18 @@ module Datagrid
|
|
184
208
|
end
|
185
209
|
|
186
210
|
# Redefines scope at instance level
|
187
|
-
#
|
211
|
+
# @example
|
188
212
|
# class MyGrid
|
189
213
|
# scope { Article.order('created_at desc') }
|
190
214
|
# end
|
191
215
|
#
|
192
216
|
# grid = MyGrid.new
|
193
217
|
# grid.scope do |scope|
|
194
|
-
# scope.where(:
|
218
|
+
# scope.where(author_id: current_user.id)
|
195
219
|
# end
|
196
220
|
# grid.assets
|
197
221
|
# # => SELECT * FROM articles WHERE author_id = ?
|
198
222
|
# # ORDER BY created_at desc
|
199
|
-
#
|
200
223
|
def scope(&block)
|
201
224
|
if block_given?
|
202
225
|
current_scope = scope
|
@@ -205,29 +228,39 @@ module Datagrid
|
|
205
228
|
}
|
206
229
|
self
|
207
230
|
else
|
208
|
-
|
209
|
-
|
231
|
+
scope = original_scope
|
232
|
+
driver.to_scope(scope)
|
210
233
|
end
|
211
234
|
end
|
212
235
|
|
236
|
+
# @!visibility private
|
237
|
+
def original_scope
|
238
|
+
check_scope_defined!
|
239
|
+
scope_value.call
|
240
|
+
end
|
241
|
+
|
213
242
|
# Resets current instance scope to default scope defined in a class
|
243
|
+
# @return [void]
|
214
244
|
def reset_scope
|
215
245
|
self.scope_value = self.class.scope_value
|
216
246
|
end
|
217
247
|
|
218
|
-
#
|
248
|
+
# @return [Boolean] true if the scope was redefined for this instance of grid object
|
219
249
|
def redefined_scope?
|
220
250
|
self.class.scope_value != scope_value
|
221
251
|
end
|
222
252
|
|
223
|
-
|
253
|
+
# @!visibility private
|
254
|
+
def driver
|
224
255
|
self.class.driver
|
225
256
|
end
|
226
257
|
|
227
|
-
|
258
|
+
# @!visibility private
|
259
|
+
def check_scope_defined!(message = nil)
|
228
260
|
self.class.send :check_scope_defined!, message
|
229
261
|
end
|
230
262
|
|
263
|
+
# @return [String] a datagrid attributes and their values in inspection form
|
231
264
|
def inspect
|
232
265
|
attrs = attributes.map do |key, value|
|
233
266
|
"#{key}: #{value.inspect}"
|
@@ -240,6 +273,6 @@ module Datagrid
|
|
240
273
|
attributes == other.attributes &&
|
241
274
|
scope == other.scope
|
242
275
|
end
|
243
|
-
end
|
276
|
+
end
|
244
277
|
end
|
245
278
|
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
module Datagrid
|
2
2
|
module Drivers
|
3
|
-
|
3
|
+
# @!visibility private
|
4
|
+
class ActiveRecord < AbstractDriver
|
4
5
|
|
5
6
|
def self.match?(scope)
|
6
7
|
return false unless defined?(::ActiveRecord)
|
@@ -18,7 +19,7 @@ module Datagrid
|
|
18
19
|
# We can only reveal it by checking if it respond to some specific
|
19
20
|
# to ActiveRecord method like #scoped
|
20
21
|
if scope.is_a?(Class)
|
21
|
-
|
22
|
+
scope.all
|
22
23
|
elsif scope.respond_to?(:scoped)
|
23
24
|
scope.scoped
|
24
25
|
else
|
@@ -42,7 +43,7 @@ module Datagrid
|
|
42
43
|
end
|
43
44
|
|
44
45
|
def asc(scope, order)
|
45
|
-
#
|
46
|
+
# Relation#order isn't able to override already applied order
|
46
47
|
# Using #reorder instead
|
47
48
|
scope.reorder(order)
|
48
49
|
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
module Datagrid
|
2
2
|
module Drivers
|
3
|
-
|
3
|
+
# @!visibility private
|
4
|
+
class Mongoid < AbstractDriver
|
4
5
|
|
5
6
|
def self.match?(scope)
|
6
7
|
return false unless defined?(::Mongoid)
|
@@ -63,7 +64,7 @@ module Datagrid
|
|
63
64
|
return nil unless type
|
64
65
|
{
|
65
66
|
[BigDecimal , String, Symbol, Range, Array, Hash, ] => :string,
|
66
|
-
[Boolean] => :boolean,
|
67
|
+
[::Mongoid::Boolean] => :boolean,
|
67
68
|
|
68
69
|
[Date] => :date,
|
69
70
|
|
@@ -1,6 +1,7 @@
|
|
1
1
|
module Datagrid
|
2
2
|
module Drivers
|
3
|
-
|
3
|
+
# @!visibility private
|
4
|
+
class Sequel < AbstractDriver
|
4
5
|
|
5
6
|
def self.match?(scope)
|
6
7
|
return false unless defined?(::Sequel)
|
@@ -81,8 +82,12 @@ module Datagrid
|
|
81
82
|
end
|
82
83
|
|
83
84
|
def batch_each(scope, batch_size, &block)
|
84
|
-
scope.
|
85
|
-
|
85
|
+
if scope.opts[:limit]
|
86
|
+
scope.each(&block)
|
87
|
+
else
|
88
|
+
scope.extension(:pagination).each_page(batch_size) do |page|
|
89
|
+
page.each(&block)
|
90
|
+
end
|
86
91
|
end
|
87
92
|
end
|
88
93
|
|
data/lib/datagrid/drivers.rb
CHANGED
data/lib/datagrid/engine.rb
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
require "rails/engine"
|
2
2
|
|
3
3
|
module Datagrid
|
4
|
+
# @!private
|
4
5
|
class Engine < ::Rails::Engine
|
5
6
|
initializer "datagrid.helpers" do
|
6
7
|
#TODO: check why it doesn't work
|
7
8
|
ActiveSupport.on_load :action_view do
|
8
9
|
include Datagrid::Helper
|
9
|
-
end
|
10
|
-
end
|
10
|
+
end
|
11
|
+
end
|
11
12
|
end
|
12
13
|
end
|
@@ -1,8 +1,12 @@
|
|
1
|
+
# An error raise when datagrid filter is defined incorrectly and
|
2
|
+
# causes filtering chain to be broken
|
1
3
|
class Datagrid::FilteringError < StandardError
|
2
4
|
end
|
3
5
|
|
4
|
-
|
6
|
+
# @!visibility private
|
7
|
+
class Datagrid::Filters::BaseFilter
|
5
8
|
|
9
|
+
class_attribute :input_helper_name, instance_writer: false
|
6
10
|
attr_accessor :grid_class, :options, :block, :name
|
7
11
|
|
8
12
|
def initialize(grid_class, name, options = {}, &block)
|
@@ -26,7 +30,7 @@ class Datagrid::Filters::BaseFilter #:nodoc:
|
|
26
30
|
result = execute(value, scope, grid_object)
|
27
31
|
|
28
32
|
return scope unless result
|
29
|
-
if result
|
33
|
+
if result == Datagrid::Filters::DEFAULT_FILTER_BLOCK
|
30
34
|
result = default_filter(value, scope, grid_object)
|
31
35
|
end
|
32
36
|
unless grid_object.driver.match?(result)
|
@@ -169,8 +173,8 @@ class Datagrid::Filters::BaseFilter #:nodoc:
|
|
169
173
|
|
170
174
|
def default_filter(value, scope, grid)
|
171
175
|
return nil if dummy?
|
172
|
-
if !driver.has_column?(scope, name) &&
|
173
|
-
|
176
|
+
if !driver.has_column?(scope, name) && scope.respond_to?(name)
|
177
|
+
scope.send(name, value)
|
174
178
|
else
|
175
179
|
default_filter_where(scope, value)
|
176
180
|
end
|
@@ -1,18 +1,19 @@
|
|
1
1
|
module Datagrid
|
2
2
|
module Filters
|
3
|
-
|
3
|
+
# @!visibility private
|
4
|
+
module CompositeFilters
|
4
5
|
|
5
6
|
def self.included(base)
|
6
7
|
base.extend ClassMethods
|
7
8
|
base.class_eval do
|
8
|
-
|
9
9
|
end
|
10
|
-
|
11
|
-
end # self.included
|
10
|
+
end
|
12
11
|
|
12
|
+
# @!visibility private
|
13
13
|
module ClassMethods
|
14
14
|
|
15
15
|
def date_range_filters(field, from_options = {}, to_options = {})
|
16
|
+
Utils.warn_once('date_range_filters is deprecated in favor of range option for date filter')
|
16
17
|
from_options = normalize_composite_filter_options(from_options, field)
|
17
18
|
to_options = normalize_composite_filter_options(to_options, field)
|
18
19
|
|
@@ -25,6 +26,7 @@ module Datagrid
|
|
25
26
|
end
|
26
27
|
|
27
28
|
def integer_range_filters(field, from_options = {}, to_options = {})
|
29
|
+
Utils.warn_once('integer_range_filters is deprecated in favor of range option for integer filter')
|
28
30
|
from_options = normalize_composite_filter_options(from_options, field)
|
29
31
|
to_options = normalize_composite_filter_options(to_options, field)
|
30
32
|
filter(from_options[:name] || :"from_#{field.to_s.tr('.', '_')}", :integer, **from_options) do |value, scope, grid|
|
@@ -37,17 +39,11 @@ module Datagrid
|
|
37
39
|
|
38
40
|
def normalize_composite_filter_options(options, field)
|
39
41
|
if options.is_a?(String) || options.is_a?(Symbol)
|
40
|
-
options = {:
|
42
|
+
options = {name: options}
|
41
43
|
end
|
42
44
|
options
|
43
45
|
end
|
44
|
-
end
|
45
|
-
|
46
|
-
module InstanceMethods
|
47
|
-
|
48
|
-
|
49
|
-
end # InstanceMethods
|
50
|
-
|
46
|
+
end
|
51
47
|
end
|
52
48
|
end
|
53
49
|
end
|
@@ -80,7 +80,7 @@ class Datagrid::Filters::DynamicFilter < Datagrid::Filters::BaseFilter
|
|
80
80
|
|
81
81
|
def operations_select
|
82
82
|
operations.map do |operation|
|
83
|
-
[I18n.t(operation, :
|
83
|
+
[I18n.t(operation, scope: "datagrid.filters.dynamic.operations").html_safe, operation]
|
84
84
|
end
|
85
85
|
end
|
86
86
|
|
@@ -1,5 +1,4 @@
|
|
1
1
|
module Datagrid::Filters::SelectOptions
|
2
|
-
|
3
2
|
def select(object)
|
4
3
|
select = self.options[:select]
|
5
4
|
if select.is_a?(Symbol)
|
@@ -11,6 +10,18 @@ module Datagrid::Filters::SelectOptions
|
|
11
10
|
end
|
12
11
|
end
|
13
12
|
|
13
|
+
def select_values(object)
|
14
|
+
options = select(object)
|
15
|
+
groups_used = grouped_choices?(options)
|
16
|
+
options.map do |option|
|
17
|
+
if groups_used
|
18
|
+
option[1].map {|o| option_text_and_value(o)}
|
19
|
+
else
|
20
|
+
option_text_and_value(option)
|
21
|
+
end
|
22
|
+
end.map(&:last)
|
23
|
+
end
|
24
|
+
|
14
25
|
def include_blank
|
15
26
|
unless prompt
|
16
27
|
options.has_key?(:include_blank) ?
|
@@ -21,4 +32,26 @@ module Datagrid::Filters::SelectOptions
|
|
21
32
|
def prompt
|
22
33
|
options.has_key?(:prompt) ? Datagrid::Utils.callable(options[:prompt]) : false
|
23
34
|
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
|
38
|
+
# Rails built-in method:
|
39
|
+
# https://github.com/rails/rails/blob/94e80269e36caf7b2d22a7ab68e6898d3a824122/actionview/lib/action_view/helpers/form_options_helper.rb#L789
|
40
|
+
# Code reuse is difficult, so it is easier to duplicate it
|
41
|
+
def option_text_and_value(option)
|
42
|
+
# Options are [text, value] pairs or strings used for both.
|
43
|
+
if !option.is_a?(String) && option.respond_to?(:first) && option.respond_to?(:last)
|
44
|
+
option = option.reject { |e| Hash === e } if Array === option
|
45
|
+
[option.first, option.last]
|
46
|
+
else
|
47
|
+
[option, option]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Rails built-in method:
|
52
|
+
# https://github.com/rails/rails/blob/f95c0b7e96eb36bc3efc0c5beffbb9e84ea664e4/actionview/lib/action_view/helpers/tags/select.rb#L36
|
53
|
+
# Code reuse is difficult, so it is easier to duplicate it
|
54
|
+
def grouped_choices?(choices)
|
55
|
+
!choices.blank? && choices.first.respond_to?(:last) && Array === choices.first.last
|
56
|
+
end
|
24
57
|
end
|