lazy-searchlogic 2.4.10
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +6 -0
- data/LICENSE +20 -0
- data/README.rdoc +308 -0
- data/Rakefile +35 -0
- data/VERSION.yml +5 -0
- data/init.rb +1 -0
- data/lib/searchlogic.rb +40 -0
- data/lib/searchlogic/active_record/consistency.rb +49 -0
- data/lib/searchlogic/active_record/named_scope_tools.rb +101 -0
- data/lib/searchlogic/core_ext/object.rb +41 -0
- data/lib/searchlogic/core_ext/proc.rb +11 -0
- data/lib/searchlogic/named_scopes/alias_scope.rb +67 -0
- data/lib/searchlogic/named_scopes/association_conditions.rb +131 -0
- data/lib/searchlogic/named_scopes/association_ordering.rb +44 -0
- data/lib/searchlogic/named_scopes/conditions.rb +227 -0
- data/lib/searchlogic/named_scopes/or_conditions.rb +141 -0
- data/lib/searchlogic/named_scopes/ordering.rb +48 -0
- data/lib/searchlogic/rails_helpers.rb +76 -0
- data/lib/searchlogic/search.rb +209 -0
- data/rails/init.rb +1 -0
- data/searchlogic.gemspec +86 -0
- data/spec/active_record/consistency_spec.rb +28 -0
- data/spec/core_ext/object_spec.rb +7 -0
- data/spec/core_ext/proc_spec.rb +9 -0
- data/spec/named_scopes/alias_scope_spec.rb +19 -0
- data/spec/named_scopes/association_conditions_spec.rb +188 -0
- data/spec/named_scopes/association_ordering_spec.rb +27 -0
- data/spec/named_scopes/conditions_spec.rb +319 -0
- data/spec/named_scopes/or_conditions_spec.rb +66 -0
- data/spec/named_scopes/ordering_spec.rb +34 -0
- data/spec/search_spec.rb +416 -0
- data/spec/spec_helper.rb +129 -0
- metadata +107 -0
@@ -0,0 +1,141 @@
|
|
1
|
+
module Searchlogic
|
2
|
+
module NamedScopes
|
3
|
+
# Handles dynamically creating named scopes for 'OR' conditions. Please see the README for a more
|
4
|
+
# detailed explanation.
|
5
|
+
module OrConditions
|
6
|
+
class NoConditionSpecifiedError < StandardError; end
|
7
|
+
class UnknownConditionError < StandardError; end
|
8
|
+
|
9
|
+
def condition?(name) # :nodoc:
|
10
|
+
super || or_condition?(name)
|
11
|
+
end
|
12
|
+
|
13
|
+
def named_scope_options(name) # :nodoc:
|
14
|
+
super || super(or_conditions(name).join("_or_"))
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
def or_condition?(name)
|
19
|
+
!or_conditions(name).nil?
|
20
|
+
end
|
21
|
+
|
22
|
+
def method_missing(name, *args, &block)
|
23
|
+
if conditions = or_conditions(name)
|
24
|
+
create_or_condition(conditions, args)
|
25
|
+
(class << self; self; end).class_eval { alias_method name, conditions.join("_or_") } if !respond_to?(name)
|
26
|
+
send(name, *args)
|
27
|
+
else
|
28
|
+
super
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def or_conditions(name)
|
33
|
+
# First determine if we should even work on the name, we want to be as quick as possible
|
34
|
+
# with this.
|
35
|
+
if (parts = split_or_condition(name)).size > 1
|
36
|
+
conditions = interpolate_or_conditions(parts)
|
37
|
+
if conditions.any?
|
38
|
+
conditions
|
39
|
+
else
|
40
|
+
nil
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def split_or_condition(name)
|
46
|
+
parts = name.to_s.split("_or_")
|
47
|
+
new_parts = []
|
48
|
+
parts.each do |part|
|
49
|
+
if part =~ /^equal_to(_any|_all)?$/
|
50
|
+
new_parts << new_parts.pop + "_or_equal_to"
|
51
|
+
else
|
52
|
+
new_parts << part
|
53
|
+
end
|
54
|
+
end
|
55
|
+
new_parts
|
56
|
+
end
|
57
|
+
|
58
|
+
# The purpose of this method is to convert the method name parts into actual condition names.
|
59
|
+
#
|
60
|
+
# Example:
|
61
|
+
#
|
62
|
+
# ["first_name", "last_name_like"]
|
63
|
+
# => ["first_name_like", "last_name_like"]
|
64
|
+
#
|
65
|
+
# ["id_gt", "first_name_begins_with", "last_name", "middle_name_like"]
|
66
|
+
# => ["id_gt", "first_name_begins_with", "last_name_like", "middle_name_like"]
|
67
|
+
#
|
68
|
+
# Basically if a column is specified without a condition the next condition in the list
|
69
|
+
# is what will be used. Once we are able to get a consistent list of conditions we can easily
|
70
|
+
# create a scope for it.
|
71
|
+
def interpolate_or_conditions(parts)
|
72
|
+
conditions = []
|
73
|
+
last_condition = nil
|
74
|
+
|
75
|
+
parts.reverse.each do |part|
|
76
|
+
if details = condition_details(part)
|
77
|
+
# We are a searchlogic defined scope
|
78
|
+
conditions << "#{details[:column]}_#{details[:condition]}"
|
79
|
+
last_condition = details[:condition]
|
80
|
+
elsif association_details = association_condition_details(part, last_condition)
|
81
|
+
path = full_association_path(part, last_condition, association_details[:association])
|
82
|
+
conditions << "#{path[:path].join("_").to_sym}_#{path[:column]}_#{path[:condition]}"
|
83
|
+
last_condition = path[:condition] || nil
|
84
|
+
elsif local_condition?(part)
|
85
|
+
# We are a custom scope
|
86
|
+
conditions << part
|
87
|
+
elsif column_names.include?(part)
|
88
|
+
# we are a column, use the last condition
|
89
|
+
if last_condition.nil?
|
90
|
+
raise NoConditionSpecifiedError.new("The '#{part}' column doesn't know which condition to use, if you use an exact column " +
|
91
|
+
"name you need to specify a condition sometime after (ex: id_or_created_at_lt), where id would use the 'lt' condition.")
|
92
|
+
end
|
93
|
+
|
94
|
+
conditions << "#{part}_#{last_condition}"
|
95
|
+
else
|
96
|
+
raise UnknownConditionError.new("The condition '#{part}' is not a valid condition, we could not find any scopes that match this.")
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
conditions.reverse
|
101
|
+
end
|
102
|
+
|
103
|
+
def full_association_path(part, last_condition, given_assoc)
|
104
|
+
path = [given_assoc.name]
|
105
|
+
part.sub!(/^#{given_assoc.name}_/, "")
|
106
|
+
klass = self
|
107
|
+
while klass = klass.send(:reflect_on_association, given_assoc.name)
|
108
|
+
klass = klass.klass
|
109
|
+
if details = klass.send(:association_condition_details, part, last_condition)
|
110
|
+
path << details[:association]
|
111
|
+
part = details[:condition]
|
112
|
+
given_assoc = details[:association]
|
113
|
+
elsif details = klass.send(:condition_details, part)
|
114
|
+
return { :path => path, :column => details[:column], :condition => details[:condition] }
|
115
|
+
end
|
116
|
+
end
|
117
|
+
{ :path => path, :column => part, :condition => last_condition }
|
118
|
+
end
|
119
|
+
|
120
|
+
def create_or_condition(scopes, args)
|
121
|
+
scopes_options = scopes.collect { |scope, *args| send(scope, *args).proxy_options }
|
122
|
+
# We're using first scope to determine column's type
|
123
|
+
scope = named_scope_options(scopes.first)
|
124
|
+
column_type = scope.respond_to?(:searchlogic_arg_type) ? scope.searchlogic_arg_type : :string
|
125
|
+
named_scope scopes.join("_or_"), searchlogic_lambda(column_type) { |*args|
|
126
|
+
merge_scopes_with_or(scopes.collect { |scope| clone.send(scope, *args) })
|
127
|
+
}
|
128
|
+
end
|
129
|
+
|
130
|
+
def merge_scopes_with_or(scopes)
|
131
|
+
scopes_options = scopes.collect { |scope| scope.scope(:find) }
|
132
|
+
conditions = scopes_options.reject { |o| o[:conditions].nil? }.collect { |o| sanitize_sql(o[:conditions]) }
|
133
|
+
scope = scopes_options.inject(scoped({})) { |current_scope, options| current_scope.scoped(options) }
|
134
|
+
options = {}
|
135
|
+
in_searchlogic_delegation { options = scope.scope(:find) }
|
136
|
+
options.delete(:readonly) unless scopes.any? { |scope| scope.proxy_options.key?(:readonly) }
|
137
|
+
options.merge(:conditions => "(" + conditions.join(") OR (") + ")")
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Searchlogic
|
2
|
+
module NamedScopes
|
3
|
+
# Handles dynamically creating named scopes for ordering by columns. Example:
|
4
|
+
#
|
5
|
+
# User.ascend_by_id
|
6
|
+
# User.descend_by_username
|
7
|
+
#
|
8
|
+
# See the README for a more detailed explanation.
|
9
|
+
module Ordering
|
10
|
+
def condition?(name) # :nodoc:
|
11
|
+
super || ordering_condition?(name)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
def ordering_condition?(name) # :nodoc:
|
16
|
+
!ordering_condition_details(name).nil?
|
17
|
+
end
|
18
|
+
|
19
|
+
def method_missing(name, *args, &block)
|
20
|
+
if name == :order
|
21
|
+
named_scope name, lambda { |scope_name|
|
22
|
+
return {} if !condition?(scope_name)
|
23
|
+
send(scope_name).proxy_options
|
24
|
+
}
|
25
|
+
send(name, *args)
|
26
|
+
elsif details = ordering_condition_details(name)
|
27
|
+
create_ordering_conditions(details[:column])
|
28
|
+
send(name, *args)
|
29
|
+
else
|
30
|
+
super
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def ordering_condition_details(name)
|
35
|
+
if name.to_s =~ /^(ascend|descend)_by_(#{column_names.join("|")})$/
|
36
|
+
{:order_as => $1, :column => $2}
|
37
|
+
elsif name.to_s =~ /^order$/
|
38
|
+
{}
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def create_ordering_conditions(column)
|
43
|
+
named_scope("ascend_by_#{column}".to_sym, {:order => "#{table_name}.#{column} ASC"})
|
44
|
+
named_scope("descend_by_#{column}".to_sym, {:order => "#{table_name}.#{column} DESC"})
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module Searchlogic
|
2
|
+
module RailsHelpers
|
3
|
+
# Creates a link that alternates between acending and descending. It basically
|
4
|
+
# alternates between calling 2 named scopes: "ascend_by_*" and "descend_by_*"
|
5
|
+
#
|
6
|
+
# By default Searchlogic gives you these named scopes for all of your columns, but
|
7
|
+
# if you wanted to create your own, it will work with those too.
|
8
|
+
#
|
9
|
+
# Examples:
|
10
|
+
#
|
11
|
+
# order @search, :by => :username
|
12
|
+
# order @search, :by => :created_at, :as => "Created"
|
13
|
+
#
|
14
|
+
# This helper accepts the following options:
|
15
|
+
#
|
16
|
+
# * <tt>:by</tt> - the name of the named scope. This helper will prepend this value with "ascend_by_" and "descend_by_"
|
17
|
+
# * <tt>:as</tt> - the text used in the link, defaults to whatever is passed to :by
|
18
|
+
# * <tt>:ascend_scope</tt> - what scope to call for ascending the data, defaults to "ascend_by_:by"
|
19
|
+
# * <tt>:descend_scope</tt> - what scope to call for descending the data, defaults to "descend_by_:by"
|
20
|
+
# * <tt>:params</tt> - hash with additional params which will be added to generated url
|
21
|
+
# * <tt>:params_scope</tt> - the name of the params key to scope the order condition by, defaults to :search
|
22
|
+
def order(search, options = {}, html_options = {})
|
23
|
+
options[:params_scope] ||= :search
|
24
|
+
if !options[:as]
|
25
|
+
id = options[:by].to_s.downcase == "id"
|
26
|
+
options[:as] = id ? options[:by].to_s.upcase : options[:by].to_s.humanize
|
27
|
+
end
|
28
|
+
options[:ascend_scope] ||= "ascend_by_#{options[:by]}"
|
29
|
+
options[:descend_scope] ||= "descend_by_#{options[:by]}"
|
30
|
+
ascending = search.order.to_s == options[:ascend_scope]
|
31
|
+
new_scope = ascending ? options[:descend_scope] : options[:ascend_scope]
|
32
|
+
selected = [options[:ascend_scope], options[:descend_scope]].include?(search.order.to_s)
|
33
|
+
if selected
|
34
|
+
css_classes = html_options[:class] ? html_options[:class].split(" ") : []
|
35
|
+
if ascending
|
36
|
+
options[:as] = "▲ #{options[:as]}"
|
37
|
+
css_classes << "ascending"
|
38
|
+
else
|
39
|
+
options[:as] = "▼ #{options[:as]}"
|
40
|
+
css_classes << "descending"
|
41
|
+
end
|
42
|
+
html_options[:class] = css_classes.join(" ")
|
43
|
+
end
|
44
|
+
url_options = {
|
45
|
+
options[:params_scope] => search.conditions.merge( { :order => new_scope } )
|
46
|
+
}.deep_merge(options[:params] || {})
|
47
|
+
link_to options[:as], url_for(url_options), html_options
|
48
|
+
end
|
49
|
+
|
50
|
+
# Automatically makes the form method :get if a Searchlogic::Search and sets
|
51
|
+
# the params scope to :search
|
52
|
+
def form_for(*args, &block)
|
53
|
+
if search_obj = args.find { |arg| arg.is_a?(Searchlogic::Search) }
|
54
|
+
options = args.extract_options!
|
55
|
+
options[:html] ||= {}
|
56
|
+
options[:html][:method] ||= :get
|
57
|
+
options[:url] ||= url_for
|
58
|
+
args.unshift(:search) if args.first == search_obj
|
59
|
+
args << options
|
60
|
+
end
|
61
|
+
super
|
62
|
+
end
|
63
|
+
|
64
|
+
# Automatically adds an "order" hidden field in your form to preserve how the data
|
65
|
+
# is being ordered.
|
66
|
+
def fields_for(*args, &block)
|
67
|
+
if search_obj = args.find { |arg| arg.is_a?(Searchlogic::Search) }
|
68
|
+
args.unshift(:search) if args.first == search_obj
|
69
|
+
concat(content_tag("div", hidden_field_tag("#{args.first}[order]", search_obj.order)))
|
70
|
+
super
|
71
|
+
else
|
72
|
+
super
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,209 @@
|
|
1
|
+
module Searchlogic
|
2
|
+
# A class that acts like a model, creates attr_accessors for named_scopes, and then
|
3
|
+
# chains together everything when an "action" method is called. It basically makes
|
4
|
+
# implementing search forms in your application effortless:
|
5
|
+
#
|
6
|
+
# search = User.search
|
7
|
+
# search.username_like = "bjohnson"
|
8
|
+
# search.all
|
9
|
+
#
|
10
|
+
# Is equivalent to:
|
11
|
+
#
|
12
|
+
# User.search(:username_like => "bjohnson").all
|
13
|
+
#
|
14
|
+
# Is equivalent to:
|
15
|
+
#
|
16
|
+
# User.username_like("bjohnson").all
|
17
|
+
class Search
|
18
|
+
# Responsible for adding a "search" method into your models.
|
19
|
+
module Implementation
|
20
|
+
# Additional method, gets aliased as "search" if that method
|
21
|
+
# is available. A lot of other libraries like to use "search"
|
22
|
+
# as well, so if you have a conflict like this, you can use
|
23
|
+
# this method directly.
|
24
|
+
def searchlogic(conditions = {})
|
25
|
+
Search.new(self, scope(:find), conditions)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Is an invalid condition is used this error will be raised. Ex:
|
30
|
+
#
|
31
|
+
# User.search(:unkown => true)
|
32
|
+
#
|
33
|
+
# Where unknown is not a valid named scope for the User model.
|
34
|
+
class UnknownConditionError < StandardError
|
35
|
+
def initialize(condition)
|
36
|
+
msg = "The #{condition} is not a valid condition. You may only use conditions that map to a named scope"
|
37
|
+
super(msg)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
attr_accessor :klass, :current_scope, :conditions
|
42
|
+
undef :id if respond_to?(:id)
|
43
|
+
|
44
|
+
# Creates a new search object for the given class. Ex:
|
45
|
+
#
|
46
|
+
# Searchlogic::Search.new(User, {}, {:username_like => "bjohnson"})
|
47
|
+
def initialize(klass, current_scope, conditions = {})
|
48
|
+
self.klass = klass
|
49
|
+
self.current_scope = current_scope
|
50
|
+
@conditions ||= {}
|
51
|
+
self.conditions = conditions if conditions.is_a?(Hash)
|
52
|
+
end
|
53
|
+
|
54
|
+
def clone
|
55
|
+
self.class.new(klass, current_scope && current_scope.clone, conditions.clone)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Returns a hash of the current conditions set.
|
59
|
+
def conditions
|
60
|
+
mass_conditions.clone.merge(@conditions)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Accepts a hash of conditions.
|
64
|
+
def conditions=(values)
|
65
|
+
values.each do |condition, value|
|
66
|
+
mass_conditions[condition.to_sym] = value
|
67
|
+
value.delete_if { |v| ignore_value?(v) } if value.is_a?(Array)
|
68
|
+
next if ignore_value?(value)
|
69
|
+
send("#{condition}=", value)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Delete a condition from the search. Since conditions map to named scopes,
|
74
|
+
# if a named scope accepts a parameter there is no way to actually delete
|
75
|
+
# the scope if you do not want it anymore. A nil value might be meaningful
|
76
|
+
# to that scope.
|
77
|
+
def delete(*names)
|
78
|
+
names.each { |name| @conditions.delete(name.to_sym) }
|
79
|
+
self
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
def method_missing(name, *args, &block)
|
84
|
+
condition_name = condition_name(name)
|
85
|
+
scope_name = scope_name(condition_name)
|
86
|
+
|
87
|
+
if setter?(name)
|
88
|
+
if scope?(scope_name)
|
89
|
+
if args.size == 1
|
90
|
+
write_condition(condition_name, type_cast(args.first, cast_type(scope_name)))
|
91
|
+
else
|
92
|
+
write_condition(condition_name, args)
|
93
|
+
end
|
94
|
+
else
|
95
|
+
raise UnknownConditionError.new(condition_name)
|
96
|
+
end
|
97
|
+
elsif scope?(scope_name) && args.size <= 1
|
98
|
+
if args.size == 0
|
99
|
+
read_condition(condition_name)
|
100
|
+
else
|
101
|
+
send("#{condition_name}=", *args)
|
102
|
+
self
|
103
|
+
end
|
104
|
+
else
|
105
|
+
scope = conditions_array.inject(klass.scoped(current_scope) || {}) do |scope, condition|
|
106
|
+
scope_name, value = condition
|
107
|
+
scope_name = normalize_scope_name(scope_name)
|
108
|
+
klass.send(scope_name, value) if !klass.respond_to?(scope_name)
|
109
|
+
arity = klass.named_scope_arity(scope_name)
|
110
|
+
|
111
|
+
if !arity || arity == 0
|
112
|
+
if value == true
|
113
|
+
scope.send(scope_name)
|
114
|
+
else
|
115
|
+
scope
|
116
|
+
end
|
117
|
+
elsif arity == -1
|
118
|
+
scope.send(scope_name, *(value.is_a?(Array) ? value : [value]))
|
119
|
+
else
|
120
|
+
scope.send(scope_name, value)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
scope.send(name, *args, &block)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# This is here as a hook to allow people to modify the order in which the conditions are called, for whatever reason.
|
128
|
+
def conditions_array
|
129
|
+
@conditions.to_a
|
130
|
+
end
|
131
|
+
|
132
|
+
def normalize_scope_name(scope_name)
|
133
|
+
case
|
134
|
+
when klass.scopes.key?(scope_name.to_sym) then scope_name.to_sym
|
135
|
+
when klass.column_names.include?(scope_name.to_s) then "#{scope_name}_equals".to_sym
|
136
|
+
else scope_name.to_sym
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def setter?(name)
|
141
|
+
!(name.to_s =~ /=$/).nil?
|
142
|
+
end
|
143
|
+
|
144
|
+
def condition_name(name)
|
145
|
+
condition = name.to_s.match(/(\w+)=?$/)
|
146
|
+
condition ? condition[1].to_sym : nil
|
147
|
+
end
|
148
|
+
|
149
|
+
def write_condition(name, value)
|
150
|
+
@conditions[name] = value
|
151
|
+
end
|
152
|
+
|
153
|
+
def read_condition(name)
|
154
|
+
@conditions[name]
|
155
|
+
end
|
156
|
+
|
157
|
+
def mass_conditions
|
158
|
+
@mass_conditions ||= {}
|
159
|
+
end
|
160
|
+
|
161
|
+
def scope_name(condition_name)
|
162
|
+
condition_name && normalize_scope_name(condition_name)
|
163
|
+
end
|
164
|
+
|
165
|
+
def scope?(scope_name)
|
166
|
+
klass.scopes.key?(scope_name) || klass.condition?(scope_name)
|
167
|
+
end
|
168
|
+
|
169
|
+
def cast_type(name)
|
170
|
+
klass.send(name, nil) if !klass.respond_to?(name) # We need to set up the named scope if it doesn't exist, so we can get a value for named_scope_options
|
171
|
+
named_scope_options = klass.named_scope_options(name)
|
172
|
+
arity = klass.named_scope_arity(name)
|
173
|
+
if !arity || arity == 0
|
174
|
+
:boolean
|
175
|
+
else
|
176
|
+
named_scope_options.respond_to?(:searchlogic_arg_type) ? named_scope_options.searchlogic_arg_type : :string
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def type_cast(value, type)
|
181
|
+
case value
|
182
|
+
when Array
|
183
|
+
value.collect { |v| type_cast(v, type) }
|
184
|
+
when Range
|
185
|
+
Range.new(type_cast(value.first, type), type_cast(value.last, type))
|
186
|
+
else
|
187
|
+
# Let's leverage ActiveRecord's type casting, so that casting is consistent
|
188
|
+
# with the other models.
|
189
|
+
column_for_type_cast = ::ActiveRecord::ConnectionAdapters::Column.new("", nil)
|
190
|
+
column_for_type_cast.instance_variable_set(:@type, type)
|
191
|
+
casted_value = column_for_type_cast.type_cast(value)
|
192
|
+
|
193
|
+
if Time.zone && casted_value.is_a?(Time)
|
194
|
+
if value.is_a?(String)
|
195
|
+
(casted_value + (Time.zone.utc_offset * -1)).in_time_zone
|
196
|
+
else
|
197
|
+
casted_value.in_time_zone
|
198
|
+
end
|
199
|
+
else
|
200
|
+
casted_value
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
def ignore_value?(value)
|
206
|
+
(value.is_a?(String) && value.blank?) || (value.is_a?(Array) && value.empty?)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|