lazy-searchlogic 2.4.10

Sign up to get free protection for your applications and to get access to all the features.
@@ -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] = "&#9650;&nbsp;#{options[:as]}"
37
+ css_classes << "ascending"
38
+ else
39
+ options[:as] = "&#9660;&nbsp;#{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