datagrid 1.6.3 → 1.8.0

Sign up to get free protection for your applications and to get access to all the features.
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