lazy-searchlogic 2.4.10

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.
@@ -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