datagrid 1.6.3 → 1.8.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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +19 -1
  3. data/Readme.markdown +11 -11
  4. data/app/views/datagrid/_enum_checkboxes.html.erb +2 -2
  5. data/app/views/datagrid/_form.html.erb +2 -3
  6. data/app/views/datagrid/_order_for.html.erb +2 -2
  7. data/app/views/datagrid/_range_filter.html.erb +3 -3
  8. data/datagrid.gemspec +10 -14
  9. data/lib/datagrid/active_model.rb +25 -29
  10. data/lib/datagrid/column_names_attribute.rb +11 -9
  11. data/lib/datagrid/columns/column.rb +10 -8
  12. data/lib/datagrid/columns.rb +142 -176
  13. data/lib/datagrid/configuration.rb +6 -1
  14. data/lib/datagrid/core.rb +63 -30
  15. data/lib/datagrid/drivers/abstract_driver.rb +2 -1
  16. data/lib/datagrid/drivers/active_record.rb +4 -3
  17. data/lib/datagrid/drivers/array.rb +2 -1
  18. data/lib/datagrid/drivers/mongo_mapper.rb +2 -1
  19. data/lib/datagrid/drivers/mongoid.rb +3 -2
  20. data/lib/datagrid/drivers/sequel.rb +8 -3
  21. data/lib/datagrid/drivers.rb +2 -1
  22. data/lib/datagrid/engine.rb +3 -2
  23. data/lib/datagrid/filters/base_filter.rb +8 -4
  24. data/lib/datagrid/filters/boolean_filter.rb +2 -1
  25. data/lib/datagrid/filters/composite_filters.rb +8 -12
  26. data/lib/datagrid/filters/dynamic_filter.rb +1 -1
  27. data/lib/datagrid/filters/extended_boolean_filter.rb +2 -1
  28. data/lib/datagrid/filters/select_options.rb +34 -1
  29. data/lib/datagrid/filters.rb +52 -30
  30. data/lib/datagrid/form_builder.rb +53 -35
  31. data/lib/datagrid/helper.rb +43 -61
  32. data/lib/datagrid/locale/en.yml +5 -1
  33. data/lib/datagrid/ordering.rb +15 -15
  34. data/lib/datagrid/renderer.rb +71 -15
  35. data/lib/datagrid/rspec.rb +4 -4
  36. data/lib/datagrid/scaffold.rb +1 -1
  37. data/lib/datagrid/utils.rb +1 -0
  38. data/lib/datagrid/version.rb +1 -1
  39. data/lib/datagrid.rb +2 -1
  40. data/templates/grid.rb.erb +1 -1
  41. data/templates/index.html.erb +1 -1
  42. metadata +8 -9
  43. 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 ClassMethods
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, :instance_writer => false
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.send :include, InstanceMethods
23
- end # self.included
23
+ base.include InstanceMethods
24
+ end
24
25
 
25
26
  module ClassMethods
26
27
 
27
- def datagrid_attribute(name, &block) #:nodoc:
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
- check_scope_defined!
53
- scope_value.call
59
+ scope = original_scope
60
+ driver.to_scope(scope)
54
61
  end
55
62
  end
56
63
 
57
- def driver #:nodoc:
58
- @driver ||= Drivers::AbstractDriver.guess_driver(scope).new
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(:category_id => category.id).count
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 # ClassMethods
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
- # Returns a hash of grid attributes including filter values
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
- # Alias for <tt>send</tt> method
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
- # Returns a scope(e.g ActiveRecord::Relation) with all applied filters
169
+ # @return [Object] a scope relation (e.g ActiveRecord::Relation) with all applied filters
145
170
  def assets
146
- driver.to_scope(scope)
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
- # Returns query parameters to link this grid from a page
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(:author_id => current_user.id)
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
- check_scope_defined!
209
- scope_value.call
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
- # Returns true if the scope was redefined for this instance of grid object
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
- def driver #:nodoc:
253
+ # @!visibility private
254
+ def driver
224
255
  self.class.driver
225
256
  end
226
257
 
227
- def check_scope_defined!(message = nil) #:nodoc:
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 # InstanceMethods
276
+ end
244
277
  end
245
278
  end
@@ -1,6 +1,7 @@
1
1
  module Datagrid
2
2
  module Drivers
3
- class AbstractDriver #:nodoc:
3
+ # @!visibility private
4
+ class AbstractDriver
4
5
 
5
6
  TIMESTAMP_CLASSES = [DateTime, Time, ActiveSupport::TimeWithZone]
6
7
 
@@ -1,6 +1,7 @@
1
1
  module Datagrid
2
2
  module Drivers
3
- class ActiveRecord < AbstractDriver #:nodoc:
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
- Rails.version >= "4.0" ? scope.all : scope.scoped({})
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
- # Rails 3.x.x don't able to override already applied order
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
- class Array < AbstractDriver #:nodoc:
3
+ # @!visibility private
4
+ class Array < AbstractDriver
4
5
 
5
6
  def self.match?(scope)
6
7
  !Datagrid::Drivers::ActiveRecord.match?(scope) && (scope.is_a?(::Array) || scope.is_a?(Enumerator))
@@ -1,6 +1,7 @@
1
1
  module Datagrid
2
2
  module Drivers
3
- class MongoMapper < AbstractDriver #:nodoc:
3
+ # @!visibility private
4
+ class MongoMapper < AbstractDriver
4
5
 
5
6
  def self.match?(scope)
6
7
  return false unless defined?(::MongoMapper)
@@ -1,6 +1,7 @@
1
1
  module Datagrid
2
2
  module Drivers
3
- class Mongoid < AbstractDriver #:nodoc:
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
- class Sequel < AbstractDriver #:nodoc:
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.extension(:pagination).each_page(batch_size) do |page|
85
- page.each(&block)
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
 
@@ -7,6 +7,7 @@ require "datagrid/drivers/sequel"
7
7
 
8
8
 
9
9
  module Datagrid
10
- module Drivers # :nodoc:
10
+ # @!visibility private
11
+ module Drivers
11
12
  end
12
13
  end
@@ -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
- class Datagrid::Filters::BaseFilter #:nodoc:
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.is_a?(Datagrid::Filters::DefaultFilterScope)
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) && driver.to_scope(scope).respond_to?(name)
173
- driver.to_scope(scope).send(name, value)
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,5 +1,6 @@
1
1
  require "datagrid/utils"
2
- class Datagrid::Filters::BooleanFilter < Datagrid::Filters::BaseFilter #:nodoc:
2
+ # @!visibility private
3
+ class Datagrid::Filters::BooleanFilter < Datagrid::Filters::BaseFilter
3
4
 
4
5
  def parse(value)
5
6
  Datagrid::Utils.booleanize(value)
@@ -1,18 +1,19 @@
1
1
  module Datagrid
2
2
  module Filters
3
- module CompositeFilters #:nodoc:
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
- base.send :include, InstanceMethods
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 = {:name => options}
42
+ options = {name: options}
41
43
  end
42
44
  options
43
45
  end
44
- end # ClassMethods
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, :scope => "datagrid.filters.dynamic.operations").html_safe, operation]
83
+ [I18n.t(operation, scope: "datagrid.filters.dynamic.operations").html_safe, operation]
84
84
  end
85
85
  end
86
86
 
@@ -1,4 +1,5 @@
1
- class Datagrid::Filters::ExtendedBooleanFilter < Datagrid::Filters::EnumFilter #:nodoc:
1
+ # @!visibility private
2
+ class Datagrid::Filters::ExtendedBooleanFilter < Datagrid::Filters::EnumFilter
2
3
 
3
4
  YES = "YES"
4
5
  NO = "NO"
@@ -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