yoomee-searchlogic 2.4.27
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.
- 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 +56 -0
- data/lib/searchlogic/active_record/association_proxy.rb +19 -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 +43 -0
- data/lib/searchlogic/core_ext/proc.rb +17 -0
- data/lib/searchlogic/named_scopes/alias_scope.rb +67 -0
- data/lib/searchlogic/named_scopes/association_conditions.rb +132 -0
- data/lib/searchlogic/named_scopes/association_ordering.rb +44 -0
- data/lib/searchlogic/named_scopes/conditions.rb +232 -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 +79 -0
- data/lib/searchlogic/search.rb +26 -0
- data/lib/searchlogic/search/base.rb +26 -0
- data/lib/searchlogic/search/conditions.rb +58 -0
- data/lib/searchlogic/search/date_parts.rb +23 -0
- data/lib/searchlogic/search/implementation.rb +14 -0
- data/lib/searchlogic/search/method_missing.rb +123 -0
- data/lib/searchlogic/search/ordering.rb +10 -0
- data/lib/searchlogic/search/scopes.rb +19 -0
- data/lib/searchlogic/search/to_yaml.rb +38 -0
- data/lib/searchlogic/search/unknown_condition_error.rb +15 -0
- data/rails/init.rb +1 -0
- data/searchlogic.gemspec +98 -0
- data/spec/searchlogic/active_record/association_proxy_spec.rb +23 -0
- data/spec/searchlogic/active_record/consistency_spec.rb +28 -0
- data/spec/searchlogic/core_ext/object_spec.rb +9 -0
- data/spec/searchlogic/core_ext/proc_spec.rb +8 -0
- data/spec/searchlogic/named_scopes/alias_scope_spec.rb +23 -0
- data/spec/searchlogic/named_scopes/association_conditions_spec.rb +203 -0
- data/spec/searchlogic/named_scopes/association_ordering_spec.rb +27 -0
- data/spec/searchlogic/named_scopes/conditions_spec.rb +319 -0
- data/spec/searchlogic/named_scopes/or_conditions_spec.rb +66 -0
- data/spec/searchlogic/named_scopes/ordering_spec.rb +34 -0
- data/spec/searchlogic/search_spec.rb +497 -0
- data/spec/spec_helper.rb +132 -0
- metadata +136 -0
@@ -0,0 +1,232 @@
|
|
1
|
+
module Searchlogic
|
2
|
+
module NamedScopes
|
3
|
+
# Handles dynamically creating named scopes for columns. It allows you to do things like:
|
4
|
+
#
|
5
|
+
# User.first_name_like("ben")
|
6
|
+
# User.id_lt(10)
|
7
|
+
#
|
8
|
+
# Notice the constants in this class, they define which conditions Searchlogic provides.
|
9
|
+
#
|
10
|
+
# See the README for a more detailed explanation.
|
11
|
+
module Conditions
|
12
|
+
COMPARISON_CONDITIONS = {
|
13
|
+
:equals => [:is, :eq],
|
14
|
+
:does_not_equal => [:not_equal_to, :is_not, :not, :ne],
|
15
|
+
:less_than => [:lt, :before],
|
16
|
+
:less_than_or_equal_to => [:lte],
|
17
|
+
:greater_than => [:gt, :after],
|
18
|
+
:greater_than_or_equal_to => [:gte],
|
19
|
+
:between => [:is_between, :in_range],
|
20
|
+
:between_inclusive => [:is_between_inclusive, :in_range_inclusive]
|
21
|
+
}
|
22
|
+
|
23
|
+
WILDCARD_CONDITIONS = {
|
24
|
+
:like => [:contains, :includes],
|
25
|
+
:not_like => [:does_not_include],
|
26
|
+
:begins_with => [:bw],
|
27
|
+
:not_begin_with => [:does_not_begin_with],
|
28
|
+
:ends_with => [:ew],
|
29
|
+
:not_end_with => [:does_not_end_with]
|
30
|
+
}
|
31
|
+
|
32
|
+
BOOLEAN_CONDITIONS = {
|
33
|
+
:null => [:nil],
|
34
|
+
:not_null => [:not_nil],
|
35
|
+
:empty => [],
|
36
|
+
:blank => [],
|
37
|
+
:not_blank => [:present]
|
38
|
+
}
|
39
|
+
|
40
|
+
CONDITIONS = {}
|
41
|
+
|
42
|
+
# Add any / all variations to every comparison and wildcard condition
|
43
|
+
COMPARISON_CONDITIONS.merge(WILDCARD_CONDITIONS).each do |condition, aliases|
|
44
|
+
CONDITIONS[condition] = aliases
|
45
|
+
CONDITIONS["#{condition}_any".to_sym] = aliases.collect { |a| "#{a}_any".to_sym }
|
46
|
+
CONDITIONS["#{condition}_all".to_sym] = aliases.collect { |a| "#{a}_all".to_sym }
|
47
|
+
end
|
48
|
+
|
49
|
+
CONDITIONS[:equals_any] = CONDITIONS[:equals_any] + [:in]
|
50
|
+
CONDITIONS[:does_not_equal_all] = CONDITIONS[:does_not_equal_all] + [:not_in]
|
51
|
+
|
52
|
+
BOOLEAN_CONDITIONS.each { |condition, aliases| CONDITIONS[condition] = aliases }
|
53
|
+
|
54
|
+
PRIMARY_CONDITIONS = CONDITIONS.keys
|
55
|
+
ALIAS_CONDITIONS = CONDITIONS.values.flatten
|
56
|
+
|
57
|
+
# Is the name of the method a valid condition that can be dynamically created?
|
58
|
+
def condition?(name)
|
59
|
+
local_condition?(name)
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
def local_condition?(name)
|
64
|
+
return false if name.blank?
|
65
|
+
scope_names = scopes.keys.reject { |k| k == :scoped }
|
66
|
+
scope_names.include?(name.to_sym) || !condition_details(name).nil? || boolean_condition?(name)
|
67
|
+
end
|
68
|
+
|
69
|
+
def boolean_condition?(name)
|
70
|
+
column = columns_hash[name.to_s] || columns_hash[name.to_s.gsub(/^not_/, "")]
|
71
|
+
column && column.type == :boolean
|
72
|
+
end
|
73
|
+
|
74
|
+
def method_missing(name, *args, &block)
|
75
|
+
if details = condition_details(name)
|
76
|
+
create_condition(details[:column], details[:condition], args)
|
77
|
+
send(name, *args)
|
78
|
+
elsif boolean_condition?(name)
|
79
|
+
column = name.to_s.gsub(/^not_/, "")
|
80
|
+
named_scope name, :conditions => {column => (name.to_s =~ /^not_/).nil?}
|
81
|
+
send(name)
|
82
|
+
else
|
83
|
+
super
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
|
88
|
+
def condition_details(method_name)
|
89
|
+
column_name_matcher = column_names.join("|")
|
90
|
+
conditions_matcher = (PRIMARY_CONDITIONS + ALIAS_CONDITIONS).join("|")
|
91
|
+
|
92
|
+
if method_name.to_s =~ /^(#{column_name_matcher})_(#{conditions_matcher})$/
|
93
|
+
{:column => $1, :condition => $2}
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def create_condition(column, condition, args)
|
98
|
+
if PRIMARY_CONDITIONS.include?(condition.to_sym)
|
99
|
+
create_primary_condition(column, condition)
|
100
|
+
elsif ALIAS_CONDITIONS.include?(condition.to_sym)
|
101
|
+
create_alias_condition(column, condition, args)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def create_primary_condition(column, condition)
|
106
|
+
column_type = columns_hash[column.to_s].type
|
107
|
+
skip_conversion = skip_time_zone_conversion_for_attributes.include?(columns_hash[column.to_s].name.to_sym)
|
108
|
+
match_keyword = ::ActiveRecord::Base.connection.adapter_name == "PostgreSQL" ? "ILIKE" : "LIKE"
|
109
|
+
|
110
|
+
scope_options = case condition.to_s
|
111
|
+
when /^equals/
|
112
|
+
scope_options(condition, column_type, lambda { |a| attribute_condition("#{table_name}.#{column}", a) }, :skip_conversion => skip_conversion)
|
113
|
+
when /^does_not_equal/
|
114
|
+
scope_options(condition, column_type, "#{table_name}.#{column} != ?", :skip_conversion => skip_conversion)
|
115
|
+
when /^less_than_or_equal_to/
|
116
|
+
scope_options(condition, column_type, "#{table_name}.#{column} <= ?", :skip_conversion => skip_conversion)
|
117
|
+
when /^less_than/
|
118
|
+
scope_options(condition, column_type, "#{table_name}.#{column} < ?", :skip_conversion => skip_conversion)
|
119
|
+
when /^greater_than_or_equal_to/
|
120
|
+
scope_options(condition, column_type, "#{table_name}.#{column} >= ?", :skip_conversion => skip_conversion)
|
121
|
+
when /^greater_than/
|
122
|
+
scope_options(condition, column_type, "#{table_name}.#{column} > ?", :skip_conversion => skip_conversion)
|
123
|
+
when /^between_inclusive/
|
124
|
+
scope_options(condition, column_type, "#{table_name}.#{column} >= ? AND #{table_name}.#{column} <= ?", :skip_conversion => skip_conversion)
|
125
|
+
when /^between/
|
126
|
+
scope_options(condition, column_type, "#{table_name}.#{column} > ? AND #{table_name}.#{column} < ?", :skip_conversion => skip_conversion)
|
127
|
+
when /^like/
|
128
|
+
scope_options(condition, column_type, "#{table_name}.#{column} #{match_keyword} ?", :skip_conversion => skip_conversion, :value_modifier => :like)
|
129
|
+
when /^not_like/
|
130
|
+
scope_options(condition, column_type, "#{table_name}.#{column} NOT #{match_keyword} ?", :skip_conversion => skip_conversion, :value_modifier => :like)
|
131
|
+
when /^begins_with/
|
132
|
+
scope_options(condition, column_type, "#{table_name}.#{column} #{match_keyword} ?", :skip_conversion => skip_conversion, :value_modifier => :begins_with)
|
133
|
+
when /^not_begin_with/
|
134
|
+
scope_options(condition, column_type, "#{table_name}.#{column} NOT #{match_keyword} ?", :skip_conversion => skip_conversion, :value_modifier => :begins_with)
|
135
|
+
when /^ends_with/
|
136
|
+
scope_options(condition, column_type, "#{table_name}.#{column} #{match_keyword} ?", :skip_conversion => skip_conversion, :value_modifier => :ends_with)
|
137
|
+
when /^not_end_with/
|
138
|
+
scope_options(condition, column_type, "#{table_name}.#{column} NOT #{match_keyword} ?", :skip_conversion => skip_conversion, :value_modifier => :ends_with)
|
139
|
+
when "null"
|
140
|
+
{:conditions => "#{table_name}.#{column} IS NULL"}
|
141
|
+
when "not_null"
|
142
|
+
{:conditions => "#{table_name}.#{column} IS NOT NULL"}
|
143
|
+
when "empty"
|
144
|
+
{:conditions => "#{table_name}.#{column} = ''"}
|
145
|
+
when "blank"
|
146
|
+
{:conditions => "#{table_name}.#{column} = '' OR #{table_name}.#{column} IS NULL"}
|
147
|
+
when "not_blank"
|
148
|
+
{:conditions => "#{table_name}.#{column} != '' AND #{table_name}.#{column} IS NOT NULL"}
|
149
|
+
end
|
150
|
+
|
151
|
+
named_scope("#{column}_#{condition}".to_sym, scope_options)
|
152
|
+
end
|
153
|
+
|
154
|
+
# This method helps cut down on defining scope options for conditions that allow *_any or *_all conditions.
|
155
|
+
# Kepp in mind that the lambdas get cached in a method, so you want to keep the contents of the lambdas as
|
156
|
+
# fast as possible, which is why I didn't do the case statement inside of the lambda.
|
157
|
+
def scope_options(condition, column_type, sql, options = {})
|
158
|
+
case condition.to_s
|
159
|
+
when /_(any|all)$/
|
160
|
+
searchlogic_lambda(column_type, :skip_conversion => options[:skip_conversion]) { |*values|
|
161
|
+
unless values.empty?
|
162
|
+
values.flatten!
|
163
|
+
values.collect! { |value| value_with_modifier(value, options[:value_modifier]) }
|
164
|
+
|
165
|
+
join = $1 == "any" ? " OR " : " AND "
|
166
|
+
|
167
|
+
scope_sql = values.collect { |value| sql.is_a?(Proc) ? sql.call(value) : sql }.join(join)
|
168
|
+
|
169
|
+
{:conditions => [scope_sql, *expand_range_bind_variables(values)]}
|
170
|
+
else
|
171
|
+
{}
|
172
|
+
end
|
173
|
+
}
|
174
|
+
else
|
175
|
+
searchlogic_lambda(column_type, :skip_conversion => options[:skip_conversion]) { |*values|
|
176
|
+
values.collect! { |value| value_with_modifier(value, options[:value_modifier]) }
|
177
|
+
|
178
|
+
scope_sql = sql.is_a?(Proc) ? sql.call(*values) : sql
|
179
|
+
|
180
|
+
{:conditions => [scope_sql, *expand_range_bind_variables(values)]}
|
181
|
+
}
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def value_with_modifier(value, modifier)
|
186
|
+
case modifier
|
187
|
+
when :like
|
188
|
+
"%#{value}%"
|
189
|
+
when :begins_with
|
190
|
+
"#{value}%"
|
191
|
+
when :ends_with
|
192
|
+
"%#{value}"
|
193
|
+
else
|
194
|
+
value
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def create_alias_condition(column, condition, args)
|
199
|
+
primary_condition = primary_condition(condition)
|
200
|
+
alias_name = "#{column}_#{condition}"
|
201
|
+
primary_name = "#{column}_#{primary_condition}"
|
202
|
+
send(primary_name, *args) # go back to method_missing and make sure we create the method
|
203
|
+
(class << self; self; end).class_eval { alias_method alias_name, primary_name }
|
204
|
+
end
|
205
|
+
|
206
|
+
# Returns the primary condition for the given alias. Ex:
|
207
|
+
#
|
208
|
+
# primary_condition(:gt) => :greater_than
|
209
|
+
def primary_condition(alias_condition)
|
210
|
+
CONDITIONS.find { |k, v| k == alias_condition.to_sym || v.include?(alias_condition.to_sym) }.first
|
211
|
+
end
|
212
|
+
|
213
|
+
# Returns the primary name for any condition on a column. You can pass it
|
214
|
+
# a primary condition, alias condition, etc, and it will return the proper
|
215
|
+
# primary condition name. This helps simply logic throughout Searchlogic. Ex:
|
216
|
+
#
|
217
|
+
# condition_scope_name(:id_gt) => :id_greater_than
|
218
|
+
# condition_scope_name(:id_greater_than) => :id_greater_than
|
219
|
+
def condition_scope_name(name)
|
220
|
+
if details = condition_details(name)
|
221
|
+
if PRIMARY_CONDITIONS.include?(name.to_sym)
|
222
|
+
name
|
223
|
+
else
|
224
|
+
"#{details[:column]}_#{primary_condition(details[:condition])}".to_sym
|
225
|
+
end
|
226
|
+
else
|
227
|
+
nil
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
@@ -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).try(: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_options) ? scope.searchlogic_options[: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,79 @@
|
|
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
|
+
|
48
|
+
options[:as] = raw(options[:as]) if defined?(RailsXss)
|
49
|
+
|
50
|
+
link_to options[:as], url_for(url_options), html_options
|
51
|
+
end
|
52
|
+
|
53
|
+
# Automatically makes the form method :get if a Searchlogic::Search and sets
|
54
|
+
# the params scope to :search
|
55
|
+
def form_for(*args, &block)
|
56
|
+
if search_obj = args.find { |arg| arg.is_a?(Searchlogic::Search) }
|
57
|
+
options = args.extract_options!
|
58
|
+
options[:html] ||= {}
|
59
|
+
options[:html][:method] ||= :get
|
60
|
+
options[:url] ||= url_for
|
61
|
+
args.unshift(:search) if args.first == search_obj
|
62
|
+
args << options
|
63
|
+
end
|
64
|
+
super
|
65
|
+
end
|
66
|
+
|
67
|
+
# Automatically adds an "order" hidden field in your form to preserve how the data
|
68
|
+
# is being ordered.
|
69
|
+
def fields_for(*args, &block)
|
70
|
+
if search_obj = args.find { |arg| arg.is_a?(Searchlogic::Search) }
|
71
|
+
args.unshift(:search) if args.first == search_obj
|
72
|
+
concat(content_tag("div", hidden_field_tag("#{args.first}[order]", search_obj.order)))
|
73
|
+
super
|
74
|
+
else
|
75
|
+
super
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|