datagrid 1.6.3 → 1.7.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/datagrid/core.rb CHANGED
@@ -4,6 +4,7 @@ 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
9
  base.extend ClassMethods
9
10
  base.class_eval do
@@ -12,7 +13,7 @@ module Datagrid
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)
@@ -20,11 +21,12 @@ module Datagrid
20
21
  end
21
22
  end
22
23
  base.send :include, InstanceMethods
23
- end # self.included
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
@@ -54,12 +61,17 @@ module Datagrid
54
61
  end
55
62
  end
56
63
 
57
- def driver #:nodoc:
64
+ # @!visibility private
65
+ def driver
58
66
  @driver ||= Drivers::AbstractDriver.guess_driver(scope).new
59
67
  end
60
68
 
61
69
  # Allows dynamic columns definition, that could not be defined at class level
62
- #
70
+ # Columns that depend on the database state or third party service
71
+ # can be defined this way.
72
+ # @param block [Proc] block that defines dynamic columns
73
+ # @return [void]
74
+ # @example
63
75
  # class MerchantsGrid
64
76
  #
65
77
  # scope { Merchant }
@@ -69,12 +81,15 @@ module Datagrid
69
81
  # dynamic do
70
82
  # PurchaseCategory.all.each do |category|
71
83
  # column(:"#{category.name.underscore}_sales") do |merchant|
72
- # merchant.purchases.where(:category_id => category.id).count
84
+ # merchant.purchases.where(category_id: category.id).count
73
85
  # end
74
86
  # end
75
87
  # end
76
88
  # end
77
89
  #
90
+ # ProductCategory.create!(name: 'Swimwear')
91
+ # ProductCategory.create!(name: 'Sportswear')
92
+ #
78
93
  # grid = MerchantsGrid.new
79
94
  # grid.data # => [
80
95
  # # [ "Name", "Swimwear Sales", "Sportswear Sales", ... ]
@@ -95,6 +110,7 @@ module Datagrid
95
110
  end
96
111
 
97
112
  protected
113
+
98
114
  def check_scope_defined!(message = nil)
99
115
  message ||= "#{self}.scope is not defined"
100
116
  raise(Datagrid::ConfigurationError, message) unless scope_value
@@ -105,7 +121,7 @@ module Datagrid
105
121
  child_class.datagrid_attributes = self.datagrid_attributes.clone
106
122
  end
107
123
 
108
- end # ClassMethods
124
+ end
109
125
 
110
126
  module InstanceMethods
111
127
 
@@ -122,8 +138,7 @@ module Datagrid
122
138
  end
123
139
  end
124
140
 
125
- # Returns a hash of grid attributes including filter values
126
- # and ordering values
141
+ # @return [Hash<Symbol, Object>] grid attributes including filter values and ordering values
127
142
  def attributes
128
143
  result = {}
129
144
  self.datagrid_attributes.each do |name|
@@ -132,21 +147,24 @@ module Datagrid
132
147
  result
133
148
  end
134
149
 
135
- # Alias for <tt>send</tt> method
150
+ # @return [Object] Any datagrid attribute value
136
151
  def [](attribute)
137
152
  self.send(attribute)
138
153
  end
139
154
 
155
+ # Assigns any datagrid attribute
156
+ # @param attribute [Symbol, String] Datagrid attribute name
157
+ # @param value [Object] Datagrid attribute value
158
+ # @return [void]
140
159
  def []=(attribute, value)
141
160
  self.send(:"#{attribute}=", value)
142
161
  end
143
162
 
144
- # Returns a scope(e.g ActiveRecord::Relation) with all applied filters
163
+ # @return [Object] a scope relation (e.g ActiveRecord::Relation) with all applied filters
145
164
  def assets
146
165
  driver.to_scope(scope)
147
166
  end
148
167
 
149
-
150
168
  # Updates datagrid attributes with a passed hash argument
151
169
  def attributes=(attributes)
152
170
  if respond_to?(:assign_attributes)
@@ -163,7 +181,7 @@ module Datagrid
163
181
  end
164
182
 
165
183
  # Returns serializable query arguments skipping all nil values
166
- #
184
+ # @example
167
185
  # grid = ProductsGrid.new(category: 'dresses', available: true)
168
186
  # grid.as_query # => {category: 'dresses', available: true}
169
187
  def as_query
@@ -174,8 +192,8 @@ module Datagrid
174
192
  attributes
175
193
  end
176
194
 
177
- # Returns query parameters to link this grid from a page
178
- #
195
+ # @return [Hash<Symbol, Hash<Symbol, Object>>] query parameters to link this grid from a page
196
+ # @example
179
197
  # grid = ProductsGrid.new(category: 'dresses', available: true)
180
198
  # Rails.application.routes.url_helpers.products_path(grid.query_params)
181
199
  # # => "/products?products_grid[category]=dresses&products_grid[available]=true"
@@ -184,19 +202,18 @@ module Datagrid
184
202
  end
185
203
 
186
204
  # Redefines scope at instance level
187
- #
205
+ # @example
188
206
  # class MyGrid
189
207
  # scope { Article.order('created_at desc') }
190
208
  # end
191
209
  #
192
210
  # grid = MyGrid.new
193
211
  # grid.scope do |scope|
194
- # scope.where(:author_id => current_user.id)
212
+ # scope.where(author_id: current_user.id)
195
213
  # end
196
214
  # grid.assets
197
215
  # # => SELECT * FROM articles WHERE author_id = ?
198
216
  # # ORDER BY created_at desc
199
- #
200
217
  def scope(&block)
201
218
  if block_given?
202
219
  current_scope = scope
@@ -211,23 +228,27 @@ module Datagrid
211
228
  end
212
229
 
213
230
  # Resets current instance scope to default scope defined in a class
231
+ # @return [void]
214
232
  def reset_scope
215
233
  self.scope_value = self.class.scope_value
216
234
  end
217
235
 
218
- # Returns true if the scope was redefined for this instance of grid object
236
+ # @return [Boolean] true if the scope was redefined for this instance of grid object
219
237
  def redefined_scope?
220
238
  self.class.scope_value != scope_value
221
239
  end
222
240
 
223
- def driver #:nodoc:
241
+ # @!visibility private
242
+ def driver
224
243
  self.class.driver
225
244
  end
226
245
 
227
- def check_scope_defined!(message = nil) #:nodoc:
246
+ # @!visibility private
247
+ def check_scope_defined!(message = nil)
228
248
  self.class.send :check_scope_defined!, message
229
249
  end
230
250
 
251
+ # @return [String] a datagrid attributes and their values in inspection form
231
252
  def inspect
232
253
  attrs = attributes.map do |key, value|
233
254
  "#{key}: #{value.inspect}"
@@ -240,6 +261,6 @@ module Datagrid
240
261
  attributes == other.attributes &&
241
262
  scope == other.scope
242
263
  end
243
- end # InstanceMethods
264
+ end
244
265
  end
245
266
  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)
@@ -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)
@@ -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,7 +1,10 @@
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
 
6
9
  attr_accessor :grid_class, :options, :block, :name
7
10
 
@@ -26,7 +29,7 @@ class Datagrid::Filters::BaseFilter #:nodoc:
26
29
  result = execute(value, scope, grid_object)
27
30
 
28
31
  return scope unless result
29
- if result.is_a?(Datagrid::Filters::DefaultFilterScope)
32
+ if result == Datagrid::Filters::DEFAULT_FILTER_BLOCK
30
33
  result = default_filter(value, scope, grid_object)
31
34
  end
32
35
  unless grid_object.driver.match?(result)
@@ -1,17 +1,19 @@
1
- class Datagrid::Filters::BooleanEnumFilter < Datagrid::Filters::EnumFilter #:nodoc:
1
+ # @!visibility private
2
+ class Datagrid::Filters::BooleanEnumFilter < Datagrid::Filters::EnumFilter
2
3
 
3
4
  YES = "YES"
4
5
  NO = "NO"
5
6
 
6
7
  def initialize(report, attribute, options = {}, &block)
8
+ Datagrid::Utils.warn_once("Datagrid eboolean filter is deprecated in favor of xboolean filter")
7
9
  options[:select] = [YES, NO].map do |key, value|
8
- [I18n.t("datagrid.filters.eboolean.#{key.downcase}", :default => key.downcase.capitalize), key]
10
+ [I18n.t("datagrid.filters.eboolean.#{key.downcase}", default: key.downcase.capitalize), key]
9
11
  end
10
12
  super(report, attribute, options, &block)
11
13
  end
12
14
 
13
15
 
14
16
  def checkbox_id(value)
15
- [object_name, name, value].join('_').underscore
17
+ [object_name, name, value].join('_').underscore
16
18
  end
17
19
  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,15 +1,15 @@
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 = {})
@@ -41,13 +41,7 @@ module Datagrid
41
41
  end
42
42
  options
43
43
  end
44
- end # ClassMethods
45
-
46
- module InstanceMethods
47
-
48
-
49
- end # InstanceMethods
50
-
44
+ end
51
45
  end
52
46
  end
53
47
  end
@@ -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
@@ -31,10 +31,10 @@ module Datagrid
31
31
  :dynamic => Filters::DynamicFilter
32
32
  }
33
33
 
34
- class DefaultFilterScope
35
- end
34
+ DEFAULT_FILTER_BLOCK = Object.new
36
35
 
37
- def self.included(base) #:nodoc:
36
+ # @!visibility private
37
+ def self.included(base)
38
38
  base.extend ClassMethods
39
39
  base.class_eval do
40
40
 
@@ -45,13 +45,18 @@ module Datagrid
45
45
 
46
46
  end
47
47
  base.send :include, InstanceMethods
48
- end # self.included
48
+ end
49
49
 
50
50
  module ClassMethods
51
51
 
52
52
  # Returns filter definition object by name
53
53
  def filter_by_name(attribute)
54
- return attribute if attribute.is_a?(Datagrid::Filters::BaseFilter)
54
+ if attribute.is_a?(Datagrid::Filters::BaseFilter)
55
+ unless ancestors.include?(attribute.grid_class)
56
+ raise "#{attribute.grid_class}##{attribute.name} filter doen't belong to #{self.class}"
57
+ end
58
+ return attribute
59
+ end
55
60
  self.filters.find do |filter|
56
61
  filter.name == attribute.to_sym
57
62
  end
@@ -108,7 +113,7 @@ module Datagrid
108
113
  end
109
114
 
110
115
  def default_filter
111
- DefaultFilterScope.new
116
+ DEFAULT_FILTER_BLOCK
112
117
  end
113
118
 
114
119
  def inspect
@@ -132,11 +137,12 @@ module Datagrid
132
137
  "#{filter.name}: #{filter.type}"
133
138
  end.join(", ")
134
139
  end
135
- end # ClassMethods
140
+ end
136
141
 
137
142
  module InstanceMethods
138
143
 
139
- def initialize(*args, &block) # :nodoc:
144
+ # @!visibility private
145
+ def initialize(*args, &block)
140
146
  self.filters_array = self.class.filters_array.clone
141
147
  self.filters_array.each do |filter|
142
148
  self[filter.name] = filter.default(self)
@@ -144,7 +150,8 @@ module Datagrid
144
150
  super(*args, &block)
145
151
  end
146
152
 
147
- def assets # :nodoc:
153
+ # @!visibility private
154
+ def assets
148
155
  apply_filters(super, filters)
149
156
  end
150
157
 
@@ -178,11 +185,18 @@ module Datagrid
178
185
  # Returns select options for specific filter or filter name
179
186
  # If given filter doesn't support select options raises `ArgumentError`
180
187
  def select_options(filter)
181
- filter = filter_by_name(filter)
182
- unless filter.class.included_modules.include?(::Datagrid::Filters::SelectOptions)
183
- raise ::Datagrid::ArgumentError, "#{filter.name} with type #{FILTER_TYPES.invert[filter.class].inspect} can not have select options"
184
- end
185
- filter.select(self)
188
+ find_select_filter(filter).select(self)
189
+ end
190
+
191
+ # Sets all options as selected for a filter that has options
192
+ def select_all(filter)
193
+ filter = find_select_filter(filter)
194
+ self[filter.name] = select_values(filter)
195
+ end
196
+
197
+ # Returns all values that can be set to a filter with select options
198
+ def select_values(filter)
199
+ find_select_filter(filter).select_values(self)
186
200
  end
187
201
 
188
202
  def default_filter
@@ -198,12 +212,21 @@ module Datagrid
198
212
 
199
213
  protected
200
214
 
215
+ def find_select_filter(filter)
216
+ filter = filter_by_name(filter)
217
+ unless filter.class.included_modules.include?(::Datagrid::Filters::SelectOptions)
218
+ raise ::Datagrid::ArgumentError,
219
+ "#{self.class.name}##{filter.name} with type #{FILTER_TYPES.invert[filter.class].inspect} can not have select options"
220
+ end
221
+ filter
222
+ end
223
+
201
224
  def apply_filters(current_scope, filters)
202
225
  filters.inject(current_scope) do |result, filter|
203
226
  filter.apply(self, result, filter_value(filter))
204
227
  end
205
228
  end
206
- end # InstanceMethods
229
+ end
207
230
 
208
231
  end
209
232
  end
@@ -2,8 +2,12 @@ require "action_view"
2
2
 
3
3
  module Datagrid
4
4
  module FormBuilder
5
-
6
- # Returns a form input html for the corresponding filter name
5
+ # @param filter_or_attribute [Datagrid::Filters::BaseFilter, String, Symbol] filter object or filter name
6
+ # @param options [Hash] options of rails form input helper
7
+ # @return [String] a form input html for the corresponding filter name
8
+ # * <tt>select</tt> for enum, xboolean filter types
9
+ # * <tt>check_box</tt> for boolean filter type
10
+ # * <tt>text_field</tt> for other filter types
7
11
  def datagrid_filter(filter_or_attribute, options = {}, &block)
8
12
  filter = datagrid_get_filter(filter_or_attribute)
9
13
  options = {
@@ -15,7 +19,10 @@ module Datagrid
15
19
  self.send(filter.form_builder_helper_name, filter, options, &block)
16
20
  end
17
21
 
18
- # Returns a form label html for the corresponding filter name
22
+ # @param filter_or_attribute [Datagrid::Filters::BaseFilter, String, Symbol] filter object or filter name
23
+ # @param text [String, nil] label text, defaults to <tt>filter.header</tt>
24
+ # @param options [Hash] options of rails <tt>label</tt> helper
25
+ # @return [String] a form label html for the corresponding filter name
19
26
  def datagrid_label(filter_or_attribute, text = nil, **options, &block)
20
27
  filter = datagrid_get_filter(filter_or_attribute)
21
28
  label(filter.name, text || filter.header, **filter.label_options, **options, &block)