├── .gitignore
├── .ruby-version
├── Appraisals
├── Gemfile
├── Gemfile.lock
├── LICENSE
├── README.rdoc
├── Rakefile
├── gemfiles
├── ar2.3.10.gemfile
├── ar2.3.10.gemfile.lock
├── ar2.3.11.gemfile
├── ar2.3.11.gemfile.lock
├── ar2.3.12.gemfile
├── ar2.3.12.gemfile.lock
├── ar2.3.14.gemfile
├── ar2.3.14.gemfile.lock
├── ar2.3.9.gemfile
└── ar2.3.9.gemfile.lock
├── init.rb
├── lib
├── searchlogic.rb
└── searchlogic
│ ├── active_record
│ ├── consistency.rb
│ ├── named_scope_tools.rb
│ └── scope.rb
│ ├── core_ext
│ ├── object.rb
│ └── proc.rb
│ ├── named_scopes
│ ├── alias_scope.rb
│ ├── association_conditions.rb
│ ├── association_ordering.rb
│ ├── base.rb
│ ├── column_conditions.rb
│ ├── or_conditions.rb
│ └── ordering.rb
│ ├── rails_helpers.rb
│ ├── search.rb
│ ├── search
│ ├── base.rb
│ ├── conditions.rb
│ ├── date_parts.rb
│ ├── implementation.rb
│ ├── method_missing.rb
│ ├── ordering.rb
│ ├── scopes.rb
│ ├── to_yaml.rb
│ └── unknown_condition_error.rb
│ └── version.rb
├── rails
└── init.rb
├── searchlogic.gemspec
└── spec
├── searchlogic
├── active_record
│ ├── association_proxy_spec.rb
│ └── consistency_spec.rb
├── core_ext
│ ├── object_spec.rb
│ └── proc_spec.rb
├── named_scopes
│ ├── alias_scope_spec.rb
│ ├── association_conditions_spec.rb
│ ├── association_ordering_spec.rb
│ ├── column_conditions_spec.rb
│ ├── or_conditions_spec.rb
│ └── ordering_spec.rb
└── search_spec.rb
└── spec_helper.rb
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | *.log
3 | pkg/*
4 | coverage/*
5 | doc/*
6 | benchmarks/*
7 | *.gem
8 |
--------------------------------------------------------------------------------
/.ruby-version:
--------------------------------------------------------------------------------
1 | 2.1.2
2 |
--------------------------------------------------------------------------------
/Appraisals:
--------------------------------------------------------------------------------
1 | [9,10,11,12,14].each do |i|
2 | appraise "ar2.3.#{i}" do
3 | gem 'activerecord', "2.3.#{i}"
4 | end
5 | end
6 |
7 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 | gemspec
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | PATH
2 | remote: .
3 | specs:
4 | searchlogic (2.5.17)
5 | activerecord (~> 2.3.12)
6 | activesupport (~> 2.3.12)
7 |
8 | GEM
9 | remote: https://rubygems.org/
10 | specs:
11 | activerecord (2.3.18)
12 | activesupport (= 2.3.18)
13 | activesupport (2.3.18)
14 | appraisal (0.4.1)
15 | bundler
16 | rake
17 | coderay (1.1.0)
18 | method_source (0.8.2)
19 | pry (0.9.12.6)
20 | coderay (~> 1.0)
21 | method_source (~> 0.8)
22 | slop (~> 3.4)
23 | rake (10.1.1)
24 | rspec (1.3.2)
25 | slop (3.4.7)
26 | sqlite3 (1.3.8)
27 | timecop (0.5.9.2)
28 |
29 | PLATFORMS
30 | ruby
31 |
32 | DEPENDENCIES
33 | appraisal (= 0.4.1)
34 | pry
35 | rspec (~> 1.3.1)
36 | searchlogic!
37 | sqlite3
38 | timecop (~> 0.5.9.1)
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2009 Ben Johnson of Binary Logic
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.rdoc:
--------------------------------------------------------------------------------
1 | = Searchlogic
2 |
3 | Searchlogic makes using ActiveRecord named scopes easier and less repetitive. It helps keep your code DRY, clean, and simple.
4 |
5 | The specs pass with ActiveRecord 2.3.9 - 2.3.14 on Ruby 1.9.3-p125, 1.9.2-p318, and 1.8.7-p358
6 |
7 | == Helpful links
8 |
9 | * Documentation: http://rdoc.info/projects/binarylogic/searchlogic
10 | * Repository: http://github.com/binarylogic/searchlogic/tree/master
11 | * Issues: http://github.com/binarylogic/searchlogic/issues
12 | * Google group: http://groups.google.com/group/searchlogic
13 | * Railscast: http://railscasts.com/episodes/176-searchlogic
14 |
15 | Before contacting me directly, please read:
16 |
17 | If you find a bug or a problem please post it in the issues section. If you need help with something, please use google groups. I check both regularly and get emails when anything happens, so that is the best place to get help. This also benefits other people in the future with the same questions / problems. Thank you.
18 |
19 | == Install & use
20 |
21 | Install the gem from rubyforge:
22 |
23 | sudo gem install searchlogic
24 |
25 | Now just set it as a dependency in your project and you are ready to go.
26 |
27 | You can also install this as a plugin:
28 |
29 | script/plugin install git://github.com/binarylogic/searchlogic.git
30 |
31 | See below for usage examples.
32 |
33 | == Search using conditions on columns
34 |
35 | Instead of explaining what Searchlogic can do, let me show you. Let's start at the top:
36 |
37 | # We have the following model
38 | User(id: integer, created_at: datetime, username: string, age: integer)
39 |
40 | # Searchlogic gives you a bunch of named scopes for free:
41 | User.username_equals("bjohnson")
42 | User.username_equals(["bjohnson", "thunt"])
43 | User.username_equals("a".."b")
44 | User.username_does_not_equal("bjohnson")
45 | User.username_begins_with("bjohnson")
46 | User.username_not_begin_with("bjohnson")
47 | User.username_like("bjohnson")
48 | User.username_not_like("bjohnson")
49 | User.username_ends_with("bjohnson")
50 | User.username_not_end_with("bjohnson")
51 | User.age_greater_than(20)
52 | User.age_greater_than_or_equal_to(20)
53 | User.age_less_than(20)
54 | User.age_less_than_or_equal_to(20)
55 | User.username_null
56 | User.username_not_null
57 | User.username_blank
58 |
59 | Any named scope Searchlogic creates is dynamic and created via method_missing. Meaning it will only create what you need. Also, keep in mind, these are just named scopes, you can chain them, call methods off of them, etc:
60 |
61 | scope = User.username_like("bjohnson").age_greater_than(20).id_less_than(55)
62 | scope.all
63 | scope.first
64 | scope.count
65 | # etc...
66 |
67 | For a complete list of conditions please see the constants in Searchlogic::NamedScopes::Conditions.
68 |
69 | == Use condition aliases
70 |
71 | Typing out 'greater_than_or_equal_to' is not fun. Instead Searchlogic provides various aliases for the conditions. For a complete list please see Searchlogic::NamedScopes::Conditions. But they are pretty straightforward:
72 |
73 | User.username_is(10) # equals
74 | User.username_eq(10) # equals
75 | User.id_lt(10) # less than
76 | User.id_lte(10) # less than or equal to
77 | User.id_gt(10) # greater than
78 | User.id_gte(10) # greater than or equal to
79 | # etc...
80 |
81 | == Search using scopes in associated classes
82 |
83 | This is my favorite part of Searchlogic. You can dynamically call scopes on associated classes and Searchlogic will take care of creating the necessary joins for you. This is REALY nice for keeping your code DRY. The best way to explain this is to show you:
84 |
85 | === Searchlogic provided scopes
86 |
87 | Let's take some basic scopes that Searchlogic provides for every model:
88 |
89 | # We have the following relationships
90 | User.has_many :orders
91 | Order.has_many :line_items
92 | LineItem
93 |
94 | # Set conditions on association columns
95 | User.orders_total_greater_than(20)
96 | User.orders_line_items_price_greater_than(20)
97 |
98 | # Order by association columns
99 | User.ascend_by_order_total
100 | User.descend_by_orders_line_items_price
101 |
102 | This is recursive, you can travel through your associations simply by typing it in the name of the method. Again these are just named scopes. You can chain them together, call methods off of them, etc.
103 |
104 | === Custom associated scopes
105 |
106 | Also, these conditions aren't limited to the scopes Searchlogic provides. You can use your own scopes. Like this:
107 |
108 | LineItem.named_scope :expensive, :conditions => "line_items.price > 500"
109 |
110 | User.orders_line_items_expensive
111 |
112 | As I stated above, Searchlogic will take care of creating the necessary joins for you. This is REALLY nice when trying to keep your code DRY, because if you wanted to use a scope like this in your User model you would have to copy over the conditions. Now you have 2 named scopes that are essentially doing the same thing. Why do that when you can dynamically access that scope using this feature?
113 |
114 | === Polymorphic associations
115 |
116 | Polymorphic associations are tough because ActiveRecord doesn't support them with the :joins or :include options. Searchlogic checks for a specific syntax and takes care of this for you. Ex:
117 |
118 | Audit.belongs_to :auditable, :polymorphic => true
119 | User.has_many :audits, :as => :auditable
120 |
121 | Audit.auditable_user_type_username_equals("ben")
122 |
123 | The above will take care of creating the inner join on the polymorphic association so that it only looks for type 'User'. On the surface it works the same as a non polymorphic association. The syntax difference being that you need to call the association and then specify the type:
124 |
125 | [polymorphic association name]_[association type]_type
126 |
127 | === Uses :joins not :include
128 |
129 | Another thing to note is that the joins created by Searchlogic do NOT use the :include option, making them much faster. Instead they leverage the :joins option, which is great for performance. To prove my point here is a quick benchmark from an application I am working on:
130 |
131 | Benchmark.bm do |x|
132 | x.report { 10.times { Event.tickets_id_gt(10).all(:include => :tickets) } }
133 | x.report { 10.times { Event.tickets_id_gt(10).all } }
134 | end
135 | user system total real
136 | 10.120000 0.170000 10.290000 ( 12.625521)
137 | 2.630000 0.050000 2.680000 ( 3.313754)
138 |
139 | If you want to use the :include option, just specify it:
140 |
141 | User.orders_line_items_price_greater_than(20).all(:include => {:orders => :line_items})
142 |
143 | Obviously, only do this if you want to actually use the included objects. Including objects into a query can be helpful with performance, especially when solving an N+1 query problem.
144 |
145 | == Order your search
146 |
147 | Just like the various conditions, Searchlogic gives you some very basic scopes for ordering your data:
148 |
149 | User.ascend_by_id
150 | User.descend_by_id
151 | User.ascend_by_orders_line_items_price
152 | # etc...
153 |
154 | == Use any or all
155 |
156 | Every condition you've seen in this readme also has 2 related conditions that you can use. Example:
157 |
158 | User.username_like_any("bjohnson", "thunt") # will return any users that have either of the strings in their username
159 | User.username_like_all("bjohnson", "thunt") # will return any users that have all of the strings in their username
160 | User.username_like_any(["bjohnson", "thunt"]) # also accepts an array
161 |
162 | This is great for checkbox filters, etc. Where you can pass an array right from your form to this condition.
163 |
164 | == Combine scopes with 'OR'
165 |
166 | In the same fashion that Searchlogic provides a tool for accessing scopes in associated classes, it also provides a tool for combining scopes with 'OR'. As we all know, when scopes are combined they are joined with 'AND', but sometimes you need to combine scopes with 'OR'. Searchlogic solves this problem:
167 |
168 | User.username_or_first_name_like("ben")
169 | => "username LIKE '%ben%' OR first_name like'%ben%'"
170 |
171 | User.id_or_age_lt_or_username_or_first_name_begins_with(10)
172 | => "id < 10 OR age < 10 OR username LIKE 'ben%' OR first_name like'ben%'"
173 |
174 | Notice you don't have to specify the explicit condition (like, gt, lt, begins with, etc.). You just need to eventually specify it. If you specify a column it will just use the next condition specified. So instead of:
175 |
176 | User.username_like_or_first_name_like("ben")
177 |
178 | You can do:
179 |
180 | User.username_or_first_name_like("ben")
181 |
182 | Again, these just map to named scopes. Use Searchlogic's dynamic scopes, use scopes on associations, use your own custom scopes. As long as it maps to a named scope it will join the conditions with 'OR'. There are no limitations.
183 |
184 | == Create scope procedures
185 |
186 | Sometimes you notice a pattern in your application where you are constantly combining certain named scopes. You want to keep the flexibility of being able to mix and match small named scopes, while at the same time being able to call a single scope for a common task. User searchlogic's scpe procedure:
187 |
188 | User.scope_procedure :awesome, lambda { first_name_begins_with("ben").last_name_begins_with("johnson").website_equals("binarylogic.com") }
189 |
190 | All that this is doing is creating a class level method, but what is nice about this method is that is more inline with your other named scopes. It also tells searchlogic that this method is 'safe' to use when using the search method. Ex:
191 |
192 | User.search(:awesome => true)
193 |
194 | Otherwise searchlogic will ignore the 'awesome' condition because there is no way to tell that its a valid scope. This is a security measure to keep users from passing in a scope with a named like 'destroy_all'.
195 |
196 | == Make searching and ordering data in your application trivial
197 |
198 | The above is great, but what about tying all of this in with a search form in your application? What would be really nice is if we could use an object that represented a single search. Like this...
199 |
200 | search = User.search(:username_like => "bjohnson", :age_less_than => 20)
201 | search.all
202 |
203 | The above is equivalent to:
204 |
205 | User.username_like("bjohnson").age_less_than(20).all
206 |
207 | You can set, read, and chain conditions off of your search too:
208 |
209 | search.username_like => "bjohnson"
210 | search.age_gt = 2 => 2
211 | search.id_gt(10).email_begins_with("bjohnson") => <#Searchlogic::Search...>
212 | search.all => An array of users
213 | search.count => integer
214 | # .. etc
215 |
216 | So let's start with the controller...
217 |
218 | === Your controller
219 |
220 | The search class just chains named scopes together for you. What's so great about that? It keeps your controllers extremely simple:
221 |
222 | class UsersController < ApplicationController
223 | def index
224 | @search = User.search(params[:search])
225 | @users = @search.all
226 | end
227 | end
228 |
229 | It doesn't get any simpler than that.
230 |
231 | === Your form
232 |
233 | Adding a search condition is as simple as adding a condition to your form. Remember all of those named scopes above? Just create fields with the same names:
234 |
235 | - form_for @search do |f|
236 | = f.text_field :username_like
237 | = f.select :age_greater_than, (0..100)
238 | = f.text_field :orders_total_greater_than
239 | = f.submit
240 |
241 | When a Searchlogic::Search object is passed to form_for it will add a hidden field for the "order" condition, to preserve the order of the data.
242 |
243 | === Additional helpers
244 |
245 | There really isn't a big need for helpers in searchlogic, other than helping you order data. If you want to order your search with a link, just specify the name of the column. Ex:
246 |
247 | = order @search, :by => :age
248 | = order @search, :by => :created_at, :as => "Created date"
249 |
250 | The first one will create a link that alternates between calling "ascend_by_age" and "descend_by_age". If you wanted to order your data by more than just a column, create your own named scopes: "ascend_by_*" and "descend_by_*". The "order" helper is a very straight forward helper, checkout the docs for some of the options.
251 |
252 | This helper is just a convenience method. It's extremely simple and there is nothing wrong with creating your own. If it doesn't do what you want, copy the code, modify it, and create your own. You could even fork the project, modify it there, and use your own gem.
253 |
254 | == Use your existing named scopes
255 |
256 | This is one of the big differences between Searchlogic v1 and v2. What about your existing named scopes? Let's say you have this:
257 |
258 | User.named_scope :four_year_olds, :conditions => {:age => 4}
259 |
260 | Again, these are all just named scopes, use it in the same way:
261 |
262 | User.search(:four_year_olds => true, :username_like => "bjohnson")
263 |
264 | Notice we pass true as the value. If a named scope does not accept any parameters (arity == 0) you can simply pass it true or false. If you pass false, the named scope will be ignored. If your named scope accepts a parameter, the value will be passed right to the named scope regardless of the value.
265 |
266 | Now just throw it in your form:
267 |
268 | - form_for @search do |f|
269 | = f.text_field :username_like
270 | = f.check_box :four_year_olds
271 | = f.submit
272 |
273 | This really allows Searchlogic to extend beyond what it provides internally. If Searchlogic doesn't provide a named scope for that crazy edge case that you need, just create your own named scope and use it. The sky is the limit.
274 |
275 | == Pagination (leverage will_paginate)
276 |
277 | Instead of recreating the wheel with pagination, Searchlogic works great with will_paginate. All that Searchlogic is doing is creating named scopes, and will_paginate works great with named scopes:
278 |
279 | User.username_like("bjohnson").age_less_than(20).paginate(:page => params[:page])
280 | User.search(:username_like => "bjohnson", :age_less_than => 20).paginate(:page => params[:page])
281 |
282 | If you don't like will_paginate, use another solution, or roll your own. Pagination really has nothing to do with searching, and the main goal for Searchlogic v2 was to keep it lean and simple. No reason to recreate the wheel and bloat the library.
283 |
284 | == Conflicts with other gems
285 |
286 | You will notice searchlogic wants to create a method called "search". So do other libraries like thinking-sphinx, etc. So searchlogic has a no conflict resolution. If the "search" method is already taken the method will be called "searchlogic" instead. So instead of
287 |
288 | User.search
289 |
290 | You would do:
291 |
292 | User.searchlogic
293 |
294 | == Under the hood
295 |
296 | Before I use a library in my application I like to glance at the source and try to at least understand the basics of how it works. If you are like me, a nice little explanation from the author is always helpful:
297 |
298 | Searchlogic utilizes method_missing to create all of these named scopes. When it hits method_missing it creates a named scope to ensure it will never hit method missing for that named scope again. Sort of a caching mechanism. It works in the same fashion as ActiveRecord's "find_by_*" methods. This way only the named scopes you need are created and nothing more.
299 |
300 | The search object is just a proxy to your model that only delegates calls that map to named scopes and nothing more. This is obviously done for security reasons. It also helps make form integration easier, by type casting values, and playing nice with form_for. This class is pretty simple as well.
301 |
302 | That's about it, the named scope options are pretty bare bones and created just like you would manually.
303 |
304 | == Credit
305 |
306 | Thanks a lot to {Tyler Hunt}[http://github.com/tylerhunt] for helping plan, design, and start the project. He was a big help.
307 |
308 | == Copyright
309 |
310 | Copyright (c) 2009 {Ben Johnson of Binary Logic}[http://www.binarylogic.com], released under the MIT license
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env rake
2 | require "bundler/gem_tasks"
3 | Bundler.setup
4 | require 'appraisal'
5 | require 'spec/rake/spectask'
6 |
7 | Spec::Rake::SpecTask.new(:spec) do |spec|
8 | spec.libs << 'lib' << 'spec'
9 | spec.spec_files = FileList['spec/**/*_spec.rb']
10 | end
11 |
12 | Spec::Rake::SpecTask.new(:rcov) do |spec|
13 | spec.libs << 'lib' << 'spec'
14 | spec.pattern = 'spec/**/*_spec.rb'
15 | spec.rcov = true
16 | end
17 |
18 | task :default => :spec
19 |
--------------------------------------------------------------------------------
/gemfiles/ar2.3.10.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source :rubygems
4 |
5 | gem "activerecord", "2.3.10"
6 |
7 | gemspec :path=>"../"
--------------------------------------------------------------------------------
/gemfiles/ar2.3.10.gemfile.lock:
--------------------------------------------------------------------------------
1 | PATH
2 | remote: /Users/john/code/searchlogic
3 | specs:
4 | searchlogic (2.5.8)
5 | activerecord (~> 2.3.8)
6 |
7 | GEM
8 | remote: http://rubygems.org/
9 | specs:
10 | activerecord (2.3.10)
11 | activesupport (= 2.3.10)
12 | activesupport (2.3.10)
13 | appraisal (0.4.1)
14 | bundler
15 | rake
16 | rake (0.9.2.2)
17 | rspec (1.3.2)
18 | sqlite3 (1.3.5)
19 |
20 | PLATFORMS
21 | ruby
22 |
23 | DEPENDENCIES
24 | activerecord (= 2.3.10)
25 | appraisal (= 0.4.1)
26 | rspec (~> 1.3.1)
27 | searchlogic!
28 | sqlite3
29 |
--------------------------------------------------------------------------------
/gemfiles/ar2.3.11.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source :rubygems
4 |
5 | gem "activerecord", "2.3.11"
6 |
7 | gemspec :path=>"../"
--------------------------------------------------------------------------------
/gemfiles/ar2.3.11.gemfile.lock:
--------------------------------------------------------------------------------
1 | PATH
2 | remote: /Users/john/code/searchlogic
3 | specs:
4 | searchlogic (2.5.8)
5 | activerecord (~> 2.3.8)
6 |
7 | GEM
8 | remote: http://rubygems.org/
9 | specs:
10 | activerecord (2.3.11)
11 | activesupport (= 2.3.11)
12 | activesupport (2.3.11)
13 | appraisal (0.4.1)
14 | bundler
15 | rake
16 | rake (0.9.2.2)
17 | rspec (1.3.2)
18 | sqlite3 (1.3.5)
19 |
20 | PLATFORMS
21 | ruby
22 |
23 | DEPENDENCIES
24 | activerecord (= 2.3.11)
25 | appraisal (= 0.4.1)
26 | rspec (~> 1.3.1)
27 | searchlogic!
28 | sqlite3
29 |
--------------------------------------------------------------------------------
/gemfiles/ar2.3.12.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source :rubygems
4 |
5 | gem "activerecord", "2.3.12"
6 |
7 | gemspec :path=>"../"
--------------------------------------------------------------------------------
/gemfiles/ar2.3.12.gemfile.lock:
--------------------------------------------------------------------------------
1 | PATH
2 | remote: /Users/john/code/searchlogic
3 | specs:
4 | searchlogic (2.5.8)
5 | activerecord (~> 2.3.8)
6 |
7 | GEM
8 | remote: http://rubygems.org/
9 | specs:
10 | activerecord (2.3.12)
11 | activesupport (= 2.3.12)
12 | activesupport (2.3.12)
13 | appraisal (0.4.1)
14 | bundler
15 | rake
16 | rake (0.9.2.2)
17 | rspec (1.3.2)
18 | sqlite3 (1.3.5)
19 |
20 | PLATFORMS
21 | ruby
22 |
23 | DEPENDENCIES
24 | activerecord (= 2.3.12)
25 | appraisal (= 0.4.1)
26 | rspec (~> 1.3.1)
27 | searchlogic!
28 | sqlite3
29 |
--------------------------------------------------------------------------------
/gemfiles/ar2.3.14.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source :rubygems
4 |
5 | gem "activerecord", "2.3.14"
6 |
7 | gemspec :path=>"../"
--------------------------------------------------------------------------------
/gemfiles/ar2.3.14.gemfile.lock:
--------------------------------------------------------------------------------
1 | PATH
2 | remote: /Users/john/code/searchlogic
3 | specs:
4 | searchlogic (2.5.8)
5 | activerecord (~> 2.3.8)
6 |
7 | GEM
8 | remote: http://rubygems.org/
9 | specs:
10 | activerecord (2.3.14)
11 | activesupport (= 2.3.14)
12 | activesupport (2.3.14)
13 | appraisal (0.4.1)
14 | bundler
15 | rake
16 | rake (0.9.2.2)
17 | rspec (1.3.2)
18 | sqlite3 (1.3.5)
19 |
20 | PLATFORMS
21 | ruby
22 |
23 | DEPENDENCIES
24 | activerecord (= 2.3.14)
25 | appraisal (= 0.4.1)
26 | rspec (~> 1.3.1)
27 | searchlogic!
28 | sqlite3
29 |
--------------------------------------------------------------------------------
/gemfiles/ar2.3.9.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source :rubygems
4 |
5 | gem "activerecord", "2.3.9"
6 |
7 | gemspec :path=>"../"
--------------------------------------------------------------------------------
/gemfiles/ar2.3.9.gemfile.lock:
--------------------------------------------------------------------------------
1 | PATH
2 | remote: /Users/john/code/searchlogic
3 | specs:
4 | searchlogic (2.5.8)
5 | activerecord (~> 2.3.8)
6 |
7 | GEM
8 | remote: http://rubygems.org/
9 | specs:
10 | activerecord (2.3.9)
11 | activesupport (= 2.3.9)
12 | activesupport (2.3.9)
13 | appraisal (0.4.1)
14 | bundler
15 | rake
16 | rake (0.9.2.2)
17 | rspec (1.3.2)
18 | sqlite3 (1.3.5)
19 |
20 | PLATFORMS
21 | ruby
22 |
23 | DEPENDENCIES
24 | activerecord (= 2.3.9)
25 | appraisal (= 0.4.1)
26 | rspec (~> 1.3.1)
27 | searchlogic!
28 | sqlite3
29 |
--------------------------------------------------------------------------------
/init.rb:
--------------------------------------------------------------------------------
1 | require "searchlogic"
--------------------------------------------------------------------------------
/lib/searchlogic.rb:
--------------------------------------------------------------------------------
1 | require 'active_support'
2 | require 'active_record'
3 | require "searchlogic/version"
4 | require "searchlogic/core_ext/proc"
5 | require "searchlogic/core_ext/object"
6 | require "searchlogic/active_record/consistency"
7 | require "searchlogic/active_record/named_scope_tools"
8 | require "searchlogic/active_record/scope"
9 | require "searchlogic/named_scopes/base"
10 | require "searchlogic/named_scopes/column_conditions"
11 | require "searchlogic/named_scopes/ordering"
12 | require "searchlogic/named_scopes/association_conditions"
13 | require "searchlogic/named_scopes/association_ordering"
14 | require "searchlogic/named_scopes/alias_scope"
15 | require "searchlogic/named_scopes/or_conditions"
16 | require "searchlogic/search/base"
17 | require "searchlogic/search/conditions"
18 | require "searchlogic/search/date_parts"
19 | require "searchlogic/search/implementation"
20 | require "searchlogic/search/method_missing"
21 | require "searchlogic/search/ordering"
22 | require "searchlogic/search/scopes"
23 | require "searchlogic/search/to_yaml"
24 | require "searchlogic/search/unknown_condition_error"
25 | require "searchlogic/search"
26 |
27 | Proc.send(:include, Searchlogic::CoreExt::Proc)
28 | Object.send(:include, Searchlogic::CoreExt::Object)
29 |
30 | module ActiveRecord # :nodoc: all
31 | class Base
32 | class << self; include Searchlogic::ActiveRecord::Consistency; end
33 | end
34 | end
35 |
36 | ActiveRecord::Base.extend(Searchlogic::ActiveRecord::Scope)
37 | ActiveRecord::Base.extend(Searchlogic::ActiveRecord::NamedScopeTools)
38 | ActiveRecord::Base.extend(Searchlogic::NamedScopes::Base)
39 | ActiveRecord::Base.extend(Searchlogic::NamedScopes::ColumnConditions)
40 | ActiveRecord::Base.extend(Searchlogic::NamedScopes::AssociationConditions)
41 | ActiveRecord::Base.extend(Searchlogic::NamedScopes::AssociationOrdering)
42 | ActiveRecord::Base.extend(Searchlogic::NamedScopes::Ordering)
43 | ActiveRecord::Base.extend(Searchlogic::NamedScopes::AliasScope)
44 | ActiveRecord::Base.extend(Searchlogic::NamedScopes::OrConditions)
45 | ActiveRecord::Base.extend(Searchlogic::Search::Implementation)
46 |
47 | # Try to use the search method, if it's available. Thinking sphinx and other plugins
48 | # like to use that method as well.
49 | if !ActiveRecord::Base.respond_to?(:search)
50 | ActiveRecord::Base.class_eval { class << self; alias_method :search, :searchlogic; end }
51 | end
52 |
53 | if defined?(ActionController)
54 | require "searchlogic/rails_helpers"
55 | ActionController::Base.helper(Searchlogic::RailsHelpers)
56 | end
57 |
--------------------------------------------------------------------------------
/lib/searchlogic/active_record/consistency.rb:
--------------------------------------------------------------------------------
1 | module Searchlogic
2 | module ActiveRecord
3 | # Active Record is pretty inconsistent with how their SQL is constructed. This
4 | # method attempts to close the gap between the various inconsistencies.
5 | module Consistency
6 | def self.included(klass)
7 | klass.class_eval do
8 | alias_method_chain :merge_joins, :singularity
9 | alias_method_chain :merge_joins, :consistent_conditions
10 | alias_method_chain :merge_joins, :merged_duplicates
11 | end
12 | end
13 |
14 | # In AR multiple joins are sometimes in a single join query, and other times they
15 | # are not. The merge_joins method in AR should account for this, but it doesn't.
16 | # This fixes that problem. This way there is one join per string, which allows
17 | # the merge_joins method to delete duplicates.
18 | def merge_joins_with_singularity(*args)
19 | joins = merge_joins_without_singularity(*args)
20 | joins.collect { |j| j.is_a?(String) ? j.split(" ") : j }.flatten.uniq
21 | end
22 |
23 | # This method ensures that the order of the conditions in the joins are the same.
24 | # The strings of the joins MUST be exactly the same for AR to remove the duplicates.
25 | # AR is not consistent in this approach, resulting in duplicate joins errors when
26 | # combining scopes.
27 | def merge_joins_with_consistent_conditions(*args)
28 | joins = merge_joins_without_consistent_conditions(*args)
29 | joins.collect do |j|
30 | if j.is_a?(String) && (j =~ / (AND|OR) /i).nil?
31 | j.gsub(/(.*) ON (.*) = (.*)/) do |m|
32 | join, cond1, cond2 = $1, $2, $3
33 | sorted = [cond1.gsub(/\(|\)/, ""), cond2.gsub(/\(|\)/, "")].sort
34 | "#{join} ON #{sorted[0]} = #{sorted[1]}"
35 | end
36 | else
37 | j
38 | end
39 | end.uniq
40 | end
41 |
42 |
43 | def merge_joins_with_merged_duplicates(*args)
44 | args << "" if !Thread.current["searchlogic_delegation"]
45 | joins = merge_joins_without_merged_duplicates(*args)
46 | end
47 | end
48 | end
49 | end
--------------------------------------------------------------------------------
/lib/searchlogic/active_record/named_scope_tools.rb:
--------------------------------------------------------------------------------
1 | module Searchlogic
2 | module ActiveRecord
3 | # Adds methods that give extra information about a classes named scopes.
4 | module NamedScopeTools
5 | # Retrieves the options passed when creating the respective named scope. Ex:
6 | #
7 | # named_scope :whatever, :conditions => {:column => value}
8 | #
9 | # This method will return:
10 | #
11 | # :conditions => {:column => value}
12 | #
13 | # ActiveRecord hides this internally in a Proc, so we have to try and pull it out with this
14 | # method.
15 | def named_scope_options(name)
16 | key = scopes.key?(name.to_sym) ? name.to_sym : condition_scope_name(name)
17 |
18 | if key && scopes[key]
19 | eval("options", scopes[key].binding)
20 | else
21 | nil
22 | end
23 | end
24 |
25 | # The arity for a named scope's proc is important, because we use the arity
26 | # to determine if the condition should be ignored when calling the search method.
27 | # If the condition is false and the arity is 0, then we skip it all together. Ex:
28 | #
29 | # User.named_scope :age_is_4, :conditions => {:age => 4}
30 | # User.search(:age_is_4 => false) == User.all
31 | # User.search(:age_is_4 => true) == User.all(:conditions => {:age => 4})
32 | #
33 | # We also use it when trying to "copy" the underlying named scope for association
34 | # conditions. This way our aliased scope accepts the same number of parameters for
35 | # the underlying scope.
36 | def named_scope_arity(name)
37 | options = named_scope_options(name)
38 | options.respond_to?(:arity) ? options.arity : nil
39 | end
40 |
41 | # When searchlogic calls a named_scope on a foreigh model it will execute that scope and then call scope(:find).
42 | # When we get these options we want this to be in an exclusive scope, especially if we are calling a condition on
43 | # the same originating model:
44 | #
45 | # Company.users_company_name_equals("name")
46 | #
47 | # If we aren't in an exclusive scope we will get unexpected results for the :joins option. Lastly, we want the named_scopes
48 | # generated by searchlogic to be symbols whenever possible. The reason for this is so that we can allow
49 | # ActiveRecord to leverage its joins library that automatically aliases joins if they appear more than once in a query.
50 | # If the joins are strings, AtiveRecord can't do anything. Because the code that does this in ActiveRecord is pretty bad
51 | # when it comes to being consisitent, searchlogic had to fix this in Searchloigc::ActiveRecord::Consistency. That said,
52 | # part of this fix is to normalize joins into strings. We do not want to do this if we are calling scopes on foreigh models.
53 | # Only when we are performing an action on it. This is what the searchlogic_delegation thread variable is all about. A
54 | # flag to let search logic know not to convert joins to strings.
55 | def in_searchlogic_delegation(&block)
56 | old = Thread.current["searchlogic_delegation"]
57 | Thread.current["searchlogic_delegation"] = true
58 | with_exclusive_scope(&block)
59 | Thread.current["searchlogic_delegation"] = old
60 | end
61 |
62 | # A convenience method for creating inner join sql to that your inner joins
63 | # are consistent with how Active Record creates them. Basically a tool for
64 | # you to use when writing your own named scopes. This way you know for sure
65 | # that duplicate joins will be removed when chaining scopes together that
66 | # use the same join.
67 | #
68 | # Also, don't worry about breaking up the joins or retriving multiple joins.
69 | # ActiveRecord will remove dupilicate joins and Searchlogic assists ActiveRecord in
70 | # breaking up your joins so that they are unique.
71 | def inner_joins(association_name)
72 | ::ActiveRecord::Associations::ClassMethods::InnerJoinDependency.new(self, association_name, nil).join_associations.collect { |assoc| assoc.association_join }
73 | end
74 |
75 | # A convenience methods to create a join on a polymorphic associations target.
76 | # Ex:
77 | #
78 | # Audit.belong_to :auditable, :polymorphic => true
79 | # User.has_many :audits, :as => :auditable
80 | #
81 | # Audit.inner_polymorphic_join(:user, :as => :auditable) # =>
82 | # "INNER JOINER users ON users.id = audits.auditable_id AND audits.auditable_type = 'User'"
83 | #
84 | # This is used internally by searchlogic to handle accessing conditions on polymorphic associations.
85 | def inner_polymorphic_join(target, options = {})
86 | options[:on] ||= table_name
87 | options[:on_table_name] ||= connection.quote_table_name(options[:on])
88 | options[:target_table] ||= connection.quote_table_name(target.to_s.pluralize)
89 | options[:as] ||= "owner"
90 | postgres = ::ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
91 | "INNER JOIN #{options[:target_table]} ON #{options[:target_table]}.id = #{options[:on_table_name]}.#{options[:as]}_id AND " +
92 | "#{options[:on_table_name]}.#{options[:as]}_type = #{postgres ? "E" : ""}'#{target.to_s.camelize}'"
93 | end
94 |
95 | # See inner_joins. Does the same thing except creates LEFT OUTER joins.
96 | def left_outer_joins(association_name)
97 | ::ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, association_name, nil).join_associations.collect { |assoc| assoc.association_join }
98 | end
99 | end
100 | end
101 | end
--------------------------------------------------------------------------------
/lib/searchlogic/active_record/scope.rb:
--------------------------------------------------------------------------------
1 | module Searchlogic
2 | module ActiveRecord
3 | # The internals to ActiveRecord like to do scopes.include?(scope_name). And this is how they check for the existence
4 | # of scopes, which is terrible. The problem is that searchlogic scopes are dynamically created. So the only solution
5 | # is to override the include? method for the scopes hash, try to create the named scope, and then check it again.
6 | # This shouldn't effect performance because once its created it never gets called again. I also cache failed names
7 | # so we don't try to create them again.
8 | module Scope
9 | def scopes
10 | read_inheritable_attribute(:scopes) || write_inheritable_attribute(:scopes, {}.tap do |h|
11 |
12 | class << h
13 | attr_accessor :active_record_class
14 | end
15 | h.active_record_class = self
16 |
17 | h.instance_eval <<-eval
18 | def include?(key)
19 | result = super
20 | return result if result
21 | active_record_class.respond_to?(key)
22 | super
23 | end
24 | eval
25 |
26 | end)
27 | end
28 | end
29 | end
30 | end
--------------------------------------------------------------------------------
/lib/searchlogic/core_ext/object.rb:
--------------------------------------------------------------------------------
1 | module Searchlogic
2 | module CoreExt
3 | # Contains extensions for the Object class that Searchlogic uses.
4 | module Object
5 | # Searchlogic needs to know the expected type of the condition value so that it can properly cast
6 | # the value in the Searchlogic::Search object. For example:
7 | #
8 | # search = User.search(:id_gt => "1")
9 | #
10 | # You would expect this:
11 | #
12 | # search.id_gt => 1
13 | #
14 | # Not this:
15 | #
16 | # search.id_gt => "1"
17 | #
18 | # Parameter values from forms are ALWAYS strings, so we have to cast them. Just like ActiveRecord
19 | # does when you instantiate a new User object.
20 | #
21 | # The problem is that ruby has no variable types, so Searchlogic needs to know what type you are expecting
22 | # for your named scope. So instead of this:
23 | #
24 | # named_scope :id_gt, lambda { |value| {:conditions => ["id > ?", value]} }
25 | #
26 | # You need to do this:
27 | #
28 | # named_scope :id_gt, searchlogic_lambda(:integer) { |value| {:conditions => ["id > ?", value]} }
29 | #
30 | # If you are wanting a string, you don't have to do anything, because Searchlogic assumes you want a string.
31 | # If you want something else, you need to specify it as I did in the above example. Comments are appreciated
32 | # on this, if you know of a better solution please let me know. But this is the best I could come up with,
33 | # without being intrusive and altering default behavior.
34 | def searchlogic_lambda(type = :string, options = {}, &block)
35 | proc = lambda(&block)
36 | proc.searchlogic_options ||= {}
37 | proc.searchlogic_options[:type] = type
38 | proc.searchlogic_options.merge!(options)
39 | proc
40 | end
41 | end
42 | end
43 | end
--------------------------------------------------------------------------------
/lib/searchlogic/core_ext/proc.rb:
--------------------------------------------------------------------------------
1 | module Searchlogic
2 | module CoreExt
3 | module Proc # :nodoc:
4 | def self.included(klass)
5 | klass.class_eval do
6 | attr_accessor :searchlogic_options
7 |
8 | def searchlogic_options
9 | @searchlogic_options ||= {}
10 | @searchlogic_options[:type] ||= :string
11 | @searchlogic_options
12 | end
13 | end
14 | end
15 | end
16 | end
17 | end
--------------------------------------------------------------------------------
/lib/searchlogic/named_scopes/alias_scope.rb:
--------------------------------------------------------------------------------
1 | module Searchlogic
2 | module NamedScopes
3 | # Adds the ability to create alias scopes that allow you to alias a named
4 | # scope or create a named scope procedure. See the alias_scope method for a more
5 | # detailed explanation.
6 | module AliasScope
7 | # In some instances you might create a class method that essentially aliases a named scope
8 | # or represents a named scope procedure. Ex:
9 | #
10 | # class User
11 | # def self.teenager
12 | # age_gte(13).age_lte(19)
13 | # end
14 | # end
15 | #
16 | # This is obviously a very basic example, but notice how we are utilizing already existing named
17 | # scopes so that we do not have to repeat ourself. This method makes a lot more sense when you are
18 | # dealing with complicated named scope.
19 | #
20 | # There is a problem though. What if you want to use this in your controller's via the 'search' method:
21 | #
22 | # User.search(:teenager => true)
23 | #
24 | # You would expect that to work, but how does Searchlogic::Search tell the difference between your
25 | # 'teenager' method and the 'destroy_all' method. It can't, there is no way to tell unless we actually
26 | # call the method, which we obviously can not do.
27 | #
28 | # The being said, we need a way to tell searchlogic that this is method is safe. Here's how you do that:
29 | #
30 | # User.alias_scope :teenager, lambda { age_gte(13).age_lte(19) }
31 | #
32 | # This feels better, it feels like our other scopes, and it provides a way to tell Searchlogic that this
33 | # is a safe method.
34 | def alias_scope(name, options = nil)
35 | alias_scopes[name.to_sym] = options
36 | (class << self; self; end).instance_eval do
37 | define_method name do |*args|
38 | case options
39 | when Symbol
40 | send(options, *args)
41 | else
42 | options.call(*args)
43 | end
44 | end
45 | end
46 | end
47 | alias_method :scope_procedure, :alias_scope
48 |
49 | def condition?(name) # :nodoc:
50 | super || alias_scope?(name)
51 | end
52 |
53 | def named_scope_options(name) # :nodoc:
54 | super || alias_scopes[name.to_sym]
55 | end
56 |
57 | private
58 | def alias_scopes # :nodoc:
59 | read_inheritable_attribute(:alias_scopes) || write_inheritable_attribute(:alias_scopes, {})
60 | end
61 |
62 | def alias_scope?(name) # :nodoc:
63 | return false if name.blank?
64 | alias_scopes.key?(name.to_sym)
65 | end
66 | end
67 | end
68 | end
69 |
--------------------------------------------------------------------------------
/lib/searchlogic/named_scopes/association_conditions.rb:
--------------------------------------------------------------------------------
1 | module Searchlogic
2 | module NamedScopes
3 | # Handles dynamically creating named scopes for associations. See the README for a detailed explanation.
4 | module AssociationConditions
5 | def condition?(name) # :nodoc:
6 | super || association_condition?(name)
7 | end
8 |
9 | private
10 | def association_condition?(name)
11 | !association_condition_details(name).nil? unless name.to_s.downcase.match("_or_")
12 | end
13 |
14 | # We need to try and create other conditions first so that we give priority to conflicting names.
15 | # Such as having a column name with the exact same name as an association condition.
16 | def create_condition(name)
17 | if result = super
18 | result
19 | elsif details = association_condition_details(name)
20 | create_association_condition(details[:association], details[:condition], details[:poly_class])
21 | end
22 | end
23 |
24 | def association_condition_details(name, last_condition = nil)
25 | non_poly_assocs = reflect_on_all_associations.reject { |assoc| assoc.options[:polymorphic] }.sort { |a, b| b.name.to_s.size <=> a.name.to_s.size }
26 | poly_assocs = reflect_on_all_associations.reject { |assoc| !assoc.options[:polymorphic] }.sort { |a, b| b.name.to_s.size <=> a.name.to_s.size }
27 | return nil if non_poly_assocs.empty? && poly_assocs.empty?
28 |
29 | name_with_condition = [name, last_condition].compact.join('_')
30 |
31 | association_name = nil
32 | poly_type = nil
33 | condition = nil
34 |
35 | if name_with_condition.to_s =~ /^(#{non_poly_assocs.collect(&:name).join("|")})_(\w+)$/ && non_poly_assocs.present?
36 | association_name = $1
37 | condition = $2
38 | elsif name_with_condition.to_s =~ /^(#{poly_assocs.collect(&:name).join("|")})_(\w+?)_type_(\w+)$/
39 | association_name = $1
40 | poly_type = $2
41 | condition = $3
42 | end
43 |
44 | if association_name && condition
45 | association = reflect_on_association(association_name.to_sym)
46 | klass = poly_type ? poly_type.camelcase.constantize : association.klass
47 | if klass.condition?(condition)
48 | {:association => association, :poly_class => poly_type && klass, :condition => condition}
49 | else
50 | nil
51 | end
52 | end
53 | end
54 |
55 | def create_association_condition(association, condition_name, poly_class = nil)
56 | name = [association.name, poly_class && "#{poly_class.name.underscore}_type", condition_name].compact.join("_")
57 | named_scope(name, association_condition_options(association, condition_name, poly_class))
58 | end
59 |
60 | def association_condition_options(association, association_condition, poly_class = nil)
61 | klass = poly_class ? poly_class : association.klass
62 | raise ArgumentError.new("The #{klass} class does not respond to the #{association_condition} scope") if !klass.respond_to?(association_condition)
63 | arity = klass.named_scope_arity(association_condition)
64 |
65 | if !arity
66 | # The underlying condition doesn't require any parameters, so let's just create a simple
67 | # named scope that is based on a hash.
68 | options = {}
69 | in_searchlogic_delegation { options = klass.send(association_condition).scope(:find) }
70 | prepare_named_scope_options(options, association, poly_class)
71 | options
72 | else
73 | scope_options = klass.named_scope_options(association_condition)
74 | scope_options = scope_options.respond_to?(:searchlogic_options) ? scope_options.searchlogic_options.clone : {}
75 | proc_args = arity_args(arity)
76 | arg_type = scope_options.delete(:type) || :string
77 |
78 | eval <<-"end_eval"
79 | searchlogic_lambda(:#{arg_type}, #{scope_options.inspect}) { |#{proc_args.join(",")}|
80 | options = {}
81 |
82 | in_searchlogic_delegation do
83 | scope = klass.send(association_condition, #{proc_args.join(",")})
84 | options = scope.scope(:find) if scope
85 | end
86 |
87 | prepare_named_scope_options(options, association, poly_class)
88 | options
89 | }
90 | end_eval
91 | end
92 | end
93 |
94 | # Used to match the new scopes parameters to the underlying scope. This way we can disguise the
95 | # new scope as best as possible instead of taking the easy way out and using *args.
96 | def arity_args(arity)
97 | args = []
98 | if arity > 0
99 | arity.times { |i| args << "arg#{i}" }
100 | else
101 | positive_arity = arity * -1
102 | positive_arity.times do |i|
103 | if i == (positive_arity - 1)
104 | args << "*arg#{i}"
105 | else
106 | args << "arg#{i}"
107 | end
108 | end
109 | end
110 | args
111 | end
112 |
113 | def prepare_named_scope_options(options, association, poly_class = nil)
114 | options.delete(:readonly) # AR likes to set :readonly to true when using the :joins option, we don't want that
115 |
116 | klass = poly_class || association.klass
117 | # sanitize the conditions locally so we get the right table name, otherwise the conditions will be evaluated on the original model
118 | options[:conditions] = klass.sanitize_sql_for_conditions(options[:conditions]) if options[:conditions].is_a?(Hash)
119 |
120 | poly_join = poly_class && inner_polymorphic_join(poly_class.name.underscore, :as => association.name)
121 |
122 | if options[:joins].is_a?(String) || array_of_strings?(options[:joins])
123 | options[:joins] = [poly_class ? poly_join : inner_joins(association.name), options[:joins]].flatten
124 | elsif poly_class
125 | options[:joins] = options[:joins].blank? ? poly_join : ([poly_join] + klass.inner_joins(options[:joins]))
126 | else
127 | options[:joins] = options[:joins].blank? ? association.name : {association.name => options[:joins]}
128 | end
129 | end
130 | end
131 | end
132 | end
133 |
--------------------------------------------------------------------------------
/lib/searchlogic/named_scopes/association_ordering.rb:
--------------------------------------------------------------------------------
1 | module Searchlogic
2 | module NamedScopes
3 | # Handles dynamically creating order named scopes for associations:
4 | #
5 | # User.has_many :orders
6 | # Order.has_many :line_items
7 | # LineItem
8 | #
9 | # User.ascend_by_orders_line_items_id
10 | #
11 | # See the README for a more detailed explanation.
12 | module AssociationOrdering
13 | def condition?(name) # :nodoc:
14 | super || association_ordering_condition?(name)
15 | end
16 |
17 | private
18 | def association_ordering_condition?(name)
19 | !association_ordering_condition_details(name).nil?
20 | end
21 |
22 | def create_condition(name)
23 | if details = association_ordering_condition_details(name)
24 | create_association_ordering_condition(details[:association], details[:order_as], details[:condition])
25 | else
26 | super
27 | end
28 | end
29 |
30 | def association_ordering_condition_details(name)
31 | associations = reflect_on_all_associations
32 | association_names = associations.collect { |assoc| assoc.name }
33 | if name.to_s =~ /^(ascend|descend)_by_(#{association_names.join("|")})_(\w+)$/
34 | {:order_as => $1, :association => associations.find { |a| a.name == $2.to_sym }, :condition => $3}
35 | end
36 | end
37 |
38 | def create_association_ordering_condition(association, order_as, condition)
39 | cond = condition
40 | poly_class = nil
41 | if condition =~ /^(\w+)_type_(\w+)$/
42 | poly_type = $1
43 | cond = $2
44 | poly_class = poly_type.camelcase.constantize if poly_type
45 | end
46 | named_scope("#{order_as}_by_#{association.name}_#{condition}", association_condition_options(association, "#{order_as}_by_#{cond}", poly_class))
47 | end
48 | end
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/lib/searchlogic/named_scopes/base.rb:
--------------------------------------------------------------------------------
1 | module Searchlogic
2 | module NamedScopes
3 | module Base
4 | def condition?(name)
5 | existing_condition?(name)
6 | end
7 |
8 | private
9 | def existing_condition?(name)
10 | return false if name.blank?
11 | @valid_scope_names ||= scopes.keys.reject { |k| k == :scoped }
12 | @valid_scope_names.include?(name.to_sym)
13 | end
14 | end
15 | end
16 | end
--------------------------------------------------------------------------------
/lib/searchlogic/named_scopes/column_conditions.rb:
--------------------------------------------------------------------------------
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 ColumnConditions
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 | }
20 |
21 | WILDCARD_CONDITIONS = {
22 | :like => [:contains, :includes],
23 | :not_like => [:does_not_include],
24 | :begins_with => [:bw],
25 | :not_begin_with => [:does_not_begin_with],
26 | :ends_with => [:ew],
27 | :not_end_with => [:does_not_end_with]
28 | }
29 |
30 | BOOLEAN_CONDITIONS = {
31 | :null => [:nil],
32 | :not_null => [:not_nil],
33 | :empty => [],
34 | :blank => [],
35 | :not_blank => [:present]
36 | }
37 |
38 | CONDITIONS = {}
39 |
40 | # Add any / all variations to every comparison and wildcard condition
41 | COMPARISON_CONDITIONS.merge(WILDCARD_CONDITIONS).each do |condition, aliases|
42 | CONDITIONS[condition] = aliases
43 | CONDITIONS["#{condition}_any".to_sym] = aliases.collect { |a| "#{a}_any".to_sym }
44 | CONDITIONS["#{condition}_all".to_sym] = aliases.collect { |a| "#{a}_all".to_sym }
45 | end
46 |
47 | CONDITIONS[:equals_any] = CONDITIONS[:equals_any] + [:in]
48 | CONDITIONS[:does_not_equal_all] = CONDITIONS[:does_not_equal_all] + [:not_in]
49 |
50 | BOOLEAN_CONDITIONS.each { |condition, aliases| CONDITIONS[condition] = aliases }
51 |
52 | PRIMARY_CONDITIONS = CONDITIONS.keys
53 | ALIAS_CONDITIONS = CONDITIONS.values.flatten
54 |
55 | # Is the name of the method a valid condition that can be dynamically created?
56 | def condition?(name)
57 | super || column_condition?(name)
58 | end
59 |
60 | # We want to return true for any conditions that can be called, and while we're at it. We might as well
61 | # create the condition so we don't have to do it again.
62 | def respond_to_missing?(*args)
63 | super || (self != ::ActiveRecord::Base && !self.abstract_class? && !create_condition(args.first).blank?)
64 | end
65 |
66 | private
67 | def column_condition?(name)
68 | return false if name.blank?
69 | !condition_details(name).nil? || boolean_condition?(name)
70 | end
71 |
72 | def boolean_condition?(name)
73 | column = columns_hash[name.to_s] || columns_hash[name.to_s.gsub(/^not_/, "")]
74 | column && column.type == :boolean
75 | end
76 |
77 | def method_missing(name, *args, &block)
78 | if create_condition(name)
79 | send(name, *args, &block)
80 | else
81 | super
82 | end
83 | end
84 |
85 | def condition_details(method_name)
86 | column_name_matcher = column_names.join("|")
87 | conditions_matcher = (PRIMARY_CONDITIONS + ALIAS_CONDITIONS).join("|")
88 |
89 | if method_name.to_s =~ /^(#{column_name_matcher})_(#{conditions_matcher})$/
90 | {:column => $1, :condition => $2}
91 | end
92 | end
93 |
94 | def create_condition(name)
95 | @conditions_already_tried ||= []
96 | return nil if @conditions_already_tried.include?(name.to_s)
97 | @conditions_already_tried << name.to_s
98 |
99 | if details = condition_details(name)
100 | if PRIMARY_CONDITIONS.include?(details[:condition].to_sym)
101 | create_primary_condition(details[:column], details[:condition])
102 | elsif ALIAS_CONDITIONS.include?(details[:condition].to_sym)
103 | create_alias_condition(details[:column], details[:condition])
104 | end
105 |
106 | elsif boolean_condition?(name)
107 | column = name.to_s.gsub(/^not_/, "")
108 | named_scope name, :conditions => {column => (name.to_s =~ /^not_/).nil?}
109 | end
110 | end
111 |
112 | def create_primary_condition(column_name, condition)
113 | column = columns_hash[column_name.to_s]
114 | column_type = column.type
115 | skip_conversion = skip_time_zone_conversion_for_attributes.include?(column.name.to_sym)
116 | match_keyword = self.connection.adapter_name == "PostgreSQL" ? "ILIKE" : "LIKE"
117 |
118 | scope_options = case condition.to_s
119 | when /^equals/
120 | scope_options(condition, column, "#{table_name}.#{column.name} = ?", :skip_conversion => skip_conversion)
121 | when /^does_not_equal/
122 | scope_options(condition, column, "#{table_name}.#{column.name} != ?", :skip_conversion => skip_conversion)
123 | when /^less_than_or_equal_to/
124 | scope_options(condition, column, "#{table_name}.#{column.name} <= ?", :skip_conversion => skip_conversion)
125 | when /^less_than/
126 | scope_options(condition, column, "#{table_name}.#{column.name} < ?", :skip_conversion => skip_conversion)
127 | when /^greater_than_or_equal_to/
128 | scope_options(condition, column, "#{table_name}.#{column.name} >= ?", :skip_conversion => skip_conversion)
129 | when /^greater_than/
130 | scope_options(condition, column, "#{table_name}.#{column.name} > ?", :skip_conversion => skip_conversion)
131 | when /^like/
132 | scope_options(condition, column, "#{table_name}.#{column.name} #{match_keyword} ?", :skip_conversion => skip_conversion, :value_modifier => :like)
133 | when /^not_like/
134 | scope_options(condition, column, "#{table_name}.#{column.name} NOT #{match_keyword} ?", :skip_conversion => skip_conversion, :value_modifier => :like)
135 | when /^begins_with/
136 | scope_options(condition, column, "#{table_name}.#{column.name} #{match_keyword} ?", :skip_conversion => skip_conversion, :value_modifier => :begins_with)
137 | when /^not_begin_with/
138 | scope_options(condition, column, "#{table_name}.#{column.name} NOT #{match_keyword} ?", :skip_conversion => skip_conversion, :value_modifier => :begins_with)
139 | when /^ends_with/
140 | scope_options(condition, column, "#{table_name}.#{column.name} #{match_keyword} ?", :skip_conversion => skip_conversion, :value_modifier => :ends_with)
141 | when /^not_end_with/
142 | scope_options(condition, column, "#{table_name}.#{column.name} NOT #{match_keyword} ?", :skip_conversion => skip_conversion, :value_modifier => :ends_with)
143 | when "null"
144 | {:conditions => "#{table_name}.#{column.name} IS NULL"}
145 | when "not_null"
146 | {:conditions => "#{table_name}.#{column.name} IS NOT NULL"}
147 | when "empty"
148 | {:conditions => "#{table_name}.#{column.name} = ''"}
149 | when "blank"
150 | {:conditions => "#{table_name}.#{column.name} = '' OR #{table_name}.#{column.name} IS NULL"}
151 | when "not_blank"
152 | {:conditions => "#{table_name}.#{column.name} != '' AND #{table_name}.#{column.name} IS NOT NULL"}
153 | end
154 |
155 | named_scope("#{column.name}_#{condition}".to_sym, scope_options)
156 | end
157 |
158 | # This method helps cut down on defining scope options for conditions that allow *_any or *_all conditions.
159 | # Kepp in mind that the lambdas get cached in a method, so you want to keep the contents of the lambdas as
160 | # fast as possible, which is why I didn't do the case statement inside of the lambda.
161 | def scope_options(condition, column, sql, options = {})
162 | equals = !(condition.to_s =~ /^equals/).nil?
163 | does_not_equal = !(condition.to_s =~ /^does_not_equal/).nil?
164 |
165 | case condition.to_s
166 | when /_(any|all)$/
167 | any = $1 == "any"
168 | join_word = any ? " OR " : " AND "
169 | searchlogic_lambda(column.type, :skip_conversion => options[:skip_conversion]) { |*values|
170 | unless values.empty?
171 | if equals && any
172 | has_nil = values.include?(nil)
173 | values = values.flatten.compact
174 | sql = attribute_condition("#{table_name}.#{column.name}", values)
175 | subs = [values]
176 |
177 | if has_nil
178 | sql += " OR " + attribute_condition("#{table_name}.#{column.name}", nil)
179 | subs << nil
180 | end
181 |
182 | {:conditions => [sql, *subs]}
183 | else
184 | values.flatten!
185 | values.collect! { |value| value_with_modifier(value, options[:value_modifier]) }
186 |
187 | scope_sql = values.collect { |value| sql }.join(join_word)
188 |
189 | {:conditions => [scope_sql, *values]}
190 | end
191 | else
192 | {}
193 | end
194 | }
195 | else
196 | searchlogic_lambda(column.type, :skip_conversion => options[:skip_conversion]) { |*values|
197 | values.collect! { |value| value_with_modifier(value, options[:value_modifier]) }
198 |
199 | new_sql = if does_not_equal && values == [nil]
200 | sql.gsub('!=', 'IS NOT')
201 | elsif equals && values == [nil]
202 | sql.gsub('=', 'IS')
203 | else
204 | sql
205 | end
206 |
207 | {:conditions => [new_sql, *values]}
208 | }
209 | end
210 | end
211 |
212 | def value_with_modifier(value, modifier)
213 | case modifier
214 | when :like
215 | "%#{value}%"
216 | when :begins_with
217 | "#{value}%"
218 | when :ends_with
219 | "%#{value}"
220 | else
221 | value
222 | end
223 | end
224 |
225 | def create_alias_condition(column_name, condition)
226 | primary_condition = primary_condition(condition)
227 | alias_name = "#{column_name}_#{condition}"
228 | primary_name = "#{column_name}_#{primary_condition}"
229 | if respond_to?(primary_name)
230 | (class << self; self; end).class_eval { alias_method alias_name, primary_name }
231 | end
232 | end
233 |
234 | # Returns the primary condition for the given alias. Ex:
235 | #
236 | # primary_condition(:gt) => :greater_than
237 | def primary_condition(alias_condition)
238 | CONDITIONS.find { |k, v| k == alias_condition.to_sym || v.include?(alias_condition.to_sym) }.first
239 | end
240 |
241 | # Returns the primary name for any condition on a column. You can pass it
242 | # a primary condition, alias condition, etc, and it will return the proper
243 | # primary condition name. This helps simply logic throughout Searchlogic. Ex:
244 | #
245 | # condition_scope_name(:id_gt) => :id_greater_than
246 | # condition_scope_name(:id_greater_than) => :id_greater_than
247 | def condition_scope_name(name)
248 | if details = condition_details(name)
249 | if PRIMARY_CONDITIONS.include?(name.to_sym)
250 | name
251 | else
252 | "#{details[:column]}_#{primary_condition(details[:condition])}".to_sym
253 | end
254 | else
255 | nil
256 | end
257 | end
258 | end
259 | end
260 | end
261 |
--------------------------------------------------------------------------------
/lib/searchlogic/named_scopes/or_conditions.rb:
--------------------------------------------------------------------------------
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 create_condition(name)
23 | if conditions = or_conditions(name)
24 | create_or_condition(conditions)
25 | alias_name = conditions.join("_or_")
26 | (class << self; self; end).class_eval { alias_method name, conditions.join("_or_") } if name != alias_name
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 column_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].name
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)
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 | options = scopes_options(scopes)
132 | merged_options = merge_options(options)
133 | merged_options.delete(:readonly)
134 | if !merged_options[:joins].blank?
135 | merged_options[:joins] = convert_joins_to_optional(merged_options[:joins])
136 | else
137 | merged_options.delete(:joins)
138 | end
139 | conditions = normalized_conditions(options)
140 | if conditions.any?
141 | merged_options[:conditions] = "(" + conditions.join(") OR (") + ")"
142 | end
143 | merged_options
144 | end
145 |
146 | def scopes_options(scopes)
147 | scopes.collect { |scope| with_exclusive_scope { scope.scope(:find) } }
148 | end
149 |
150 | def convert_joins_to_optional(joins)
151 | joins ||= []
152 |
153 | (joins || []).collect { |join| join.gsub(/INNER JOIN/, 'LEFT OUTER JOIN') }
154 | end
155 |
156 | def merge_options(options)
157 | with_exclusive_scope do
158 | options.inject(scoped({:joins => "", :conditions => ""})) do |current_scope, option|
159 | current_scope.scoped(option)
160 | end.scope(:find)
161 | end
162 | end
163 |
164 | def normalized_conditions(options)
165 | options.collect { |option| option[:conditions] && sanitize_sql(option[:conditions]) }.compact
166 | end
167 | end
168 | end
169 | end
170 |
--------------------------------------------------------------------------------
/lib/searchlogic/named_scopes/ordering.rb:
--------------------------------------------------------------------------------
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 create_condition(name)
20 | if name == :order
21 | alias_scope name, lambda { |scope_name|
22 | return scoped({}) if !condition?(scope_name)
23 | send(scope_name)
24 | }
25 | elsif details = ordering_condition_details(name)
26 | create_ordering_conditions(details[:column])
27 | else
28 | super
29 | end
30 | end
31 |
32 | def ordering_condition_details(name)
33 | if name.to_s =~ /^(ascend|descend)_by_(#{column_names.join("|")})$/
34 | {:order_as => $1, :column => $2}
35 | elsif name.to_s =~ /^order$/
36 | {}
37 | end
38 | rescue ::ActiveRecord::StatementInvalid
39 | end
40 |
41 | def create_ordering_conditions(column)
42 | named_scope("ascend_by_#{column}".to_sym, {:order => "#{table_name}.#{column} ASC"})
43 | named_scope("descend_by_#{column}".to_sym, {:order => "#{table_name}.#{column} DESC"})
44 | end
45 | end
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/lib/searchlogic/rails_helpers.rb:
--------------------------------------------------------------------------------
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 | # * :by - the name of the named scope. This helper will prepend this value with "ascend_by_" and "descend_by_"
17 | # * :as - the text used in the link, defaults to whatever is passed to :by
18 | # * :ascend_scope - what scope to call for ascending the data, defaults to "ascend_by_:by"
19 | # * :descend_scope - what scope to call for descending the data, defaults to "descend_by_:by"
20 | # * :params - hash with additional params which will be added to generated url
21 | # * :params_scope - 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 | options = args.extract_options!
73 | if !options[:skip_order_field]
74 | concat(content_tag("div", hidden_field_tag("#{args.first}[order]", search_obj.order), :style => "display: inline"))
75 | end
76 | args << options
77 | super
78 | else
79 | super
80 | end
81 | end
82 | end
83 | end
84 |
--------------------------------------------------------------------------------
/lib/searchlogic/search.rb:
--------------------------------------------------------------------------------
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 | include Base
19 | include Conditions
20 | include DateParts
21 | include MethodMissing
22 | include Scopes
23 | include Ordering
24 | include ToYaml
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/lib/searchlogic/search/base.rb:
--------------------------------------------------------------------------------
1 | module Searchlogic
2 | class Search
3 | module Base
4 | def self.included(klass)
5 | klass.class_eval do
6 | attr_accessor :klass, :current_scope
7 | undef :id if respond_to?(:id)
8 | end
9 | end
10 |
11 | # Creates a new search object for the given class. Ex:
12 | #
13 | # Searchlogic::Search.new(User, {}, {:username_like => "bjohnson"})
14 | def initialize(klass, current_scope, conditions = {})
15 | self.klass = klass
16 | self.current_scope = current_scope
17 | @conditions ||= {}
18 | self.conditions = conditions if conditions.is_a?(Hash)
19 | end
20 |
21 | def clone
22 | self.class.new(klass, current_scope && current_scope.clone, conditions.clone)
23 | end
24 | end
25 | end
26 | end
--------------------------------------------------------------------------------
/lib/searchlogic/search/conditions.rb:
--------------------------------------------------------------------------------
1 | module Searchlogic
2 | class Search
3 | module Conditions
4 | # Returns a hash of the current conditions set.
5 | def conditions
6 | mass_conditions.clone.merge(@conditions)
7 | end
8 |
9 | def compact_conditions
10 | Hash[conditions.select { |k,v| !v.blank? }]
11 | end
12 |
13 | # Accepts a hash of conditions.
14 | def conditions=(values)
15 | values.each do |condition, value|
16 | mass_conditions[condition.to_sym] = value
17 | value.delete_if { |v| ignore_value?(v) } if value.is_a?(Array)
18 | next if ignore_value?(value)
19 | send("#{condition}=", value)
20 | end
21 | end
22 |
23 | # Delete a condition from the search. Since conditions map to named scopes,
24 | # if a named scope accepts a parameter there is no way to actually delete
25 | # the scope if you do not want it anymore. A nil value might be meaningful
26 | # to that scope.
27 | def delete(*names)
28 | names.each do |name|
29 | @conditions.delete(name.to_sym)
30 | mass_conditions.delete(name)
31 | end
32 | self
33 | end
34 |
35 | private
36 | # This is here as a hook to allow people to modify the order in which the conditions are called, for whatever reason.
37 | def conditions_array
38 | @conditions.to_a
39 | end
40 |
41 | def write_condition(name, value)
42 | @conditions[name] = value
43 | end
44 |
45 | def read_condition(name)
46 | @conditions[name]
47 | end
48 |
49 | def mass_conditions
50 | @mass_conditions ||= {}
51 | end
52 |
53 | def ignore_value?(value)
54 | (value.is_a?(String) && value.blank?) || (value.is_a?(Array) && value.empty?)
55 | end
56 | end
57 | end
58 | end
--------------------------------------------------------------------------------
/lib/searchlogic/search/date_parts.rb:
--------------------------------------------------------------------------------
1 | module Searchlogic
2 | class Search
3 | module DateParts
4 | def conditions=(values)
5 | values.clone.each do |condition, value|
6 | # if a condition name ends with "(1i)", assume it's date / datetime
7 | if condition =~ /(.*)\(1i\)$/
8 | date_scope_name = $1
9 | date_parts = (1..6).to_a.map do |idx|
10 | values.delete("#{ date_scope_name }(#{ idx }i)")
11 | end.reject{|s| s.blank? }.map{|s| s.to_i }
12 |
13 | # did we get enough info to build a time?
14 | if date_parts.length >= 3
15 | values[date_scope_name] = Time.zone.local(*date_parts)
16 | end
17 | end
18 | end
19 | super
20 | end
21 | end
22 | end
23 | end
--------------------------------------------------------------------------------
/lib/searchlogic/search/implementation.rb:
--------------------------------------------------------------------------------
1 | module Searchlogic
2 | class Search
3 | # Responsible for adding a "search" method into your models.
4 | module Implementation
5 | # Additional method, gets aliased as "search" if that method
6 | # is available. A lot of other libraries like to use "search"
7 | # as well, so if you have a conflict like this, you can use
8 | # this method directly.
9 | def searchlogic(conditions = {})
10 | Search.new(self, scope(:find), conditions)
11 | end
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/lib/searchlogic/search/method_missing.rb:
--------------------------------------------------------------------------------
1 | module Searchlogic
2 | class Search
3 | module MethodMissing
4 | def respond_to_missing?(*args)
5 | super || scope?(normalize_scope_name(args.first))
6 | rescue Searchlogic::NamedScopes::OrConditions::UnknownConditionError
7 | false
8 | end
9 |
10 | private
11 | def method_missing(name, *args, &block)
12 | condition_name = condition_name(name)
13 | scope_name = scope_name(condition_name)
14 |
15 | if setter?(name)
16 | if scope?(scope_name)
17 | if args.size == 1
18 | write_condition(
19 | condition_name,
20 | type_cast(
21 | args.first,
22 | cast_type(scope_name),
23 | scope_options(scope_name).respond_to?(:searchlogic_options) ? scope_options(scope_name).searchlogic_options : {}
24 | )
25 | )
26 | else
27 | write_condition(condition_name, args)
28 | end
29 | else
30 | raise UnknownConditionError.new(condition_name)
31 | end
32 | elsif scope?(scope_name) && args.size <= 1
33 | if args.size == 0
34 | read_condition(condition_name)
35 | else
36 | send("#{condition_name}=", *args)
37 | self
38 | end
39 | else
40 | scope = conditions_array.inject(klass.scoped(current_scope) || {}) do |scope, condition|
41 | scope_name, value = condition
42 | scope_name = normalize_scope_name(scope_name)
43 | klass.send(scope_name, value) if !klass.respond_to?(scope_name)
44 | arity = klass.named_scope_arity(scope_name)
45 |
46 | if !arity || arity == 0
47 | if value == true
48 | scope.send(scope_name)
49 | else
50 | scope
51 | end
52 | elsif arity == -1
53 | scope.send(scope_name, *(value.is_a?(Array) ? value : [value]))
54 | else
55 | scope.send(scope_name, value)
56 | end
57 | end
58 | scope.send(name, *args, &block)
59 | end
60 | end
61 |
62 | def normalize_scope_name(scope_name)
63 | case
64 | when klass.scopes.key?(scope_name.to_sym) then scope_name.to_sym
65 | when klass.column_names.include?(scope_name.to_s) then "#{scope_name}_equals".to_sym
66 | else scope_name.to_sym
67 | end
68 | end
69 |
70 | def setter?(name)
71 | !(name.to_s =~ /=$/).nil?
72 | end
73 |
74 | def condition_name(name)
75 | condition = name.to_s.match(/(\w+)=?$/)
76 | condition ? condition[1].to_sym : nil
77 | end
78 |
79 | def cast_type(name)
80 | named_scope_options = scope_options(name)
81 | arity = klass.named_scope_arity(name)
82 | if !arity || arity == 0
83 | :boolean
84 | else
85 | named_scope_options.respond_to?(:searchlogic_options) ? named_scope_options.searchlogic_options[:type] : :string
86 | end
87 | end
88 |
89 | def type_cast(value, type, options = {})
90 | case value
91 | when Array
92 | value.collect { |v| type_cast(v, type) }.uniq
93 | when Range
94 | Range.new(type_cast(value.first, type), type_cast(value.last, type))
95 | else
96 | # Let's leverage ActiveRecord's type casting, so that casting is consistent
97 | # with the other models.
98 | column_for_type_cast = ::ActiveRecord::ConnectionAdapters::Column.new("", nil)
99 | column_for_type_cast.instance_variable_set(:@type, type)
100 | casted_value = column_for_type_cast.type_cast(value)
101 |
102 | if Time.zone && casted_value.is_a?(Time)
103 | if value.is_a?(String)
104 | # if its a string, we should assume the user means the local time
105 | # we need to update the object to include the proper time zone without changing
106 | # the time
107 | (casted_value + (Time.zone.utc_offset * -1)).in_time_zone(Time.zone)
108 | else
109 | casted_value.in_time_zone
110 | end
111 | else
112 | casted_value
113 | end
114 | end
115 | end
116 | end
117 | end
118 | end
--------------------------------------------------------------------------------
/lib/searchlogic/search/ordering.rb:
--------------------------------------------------------------------------------
1 | module Searchlogic
2 | class Search
3 | module Ordering
4 | # Returns the column we are currently ordering by
5 | def ordering_by
6 | @ordering_by ||= order && order.to_s.match(/^(ascend|descend)_by_(.*)$/).try(:[], 2)
7 | end
8 |
9 | def ordering_direction
10 | @ordering_direction ||= order && order.to_s.match(/^(ascend|descend)_by_/).try(:[], 1)
11 | end
12 | end
13 | end
14 | end
--------------------------------------------------------------------------------
/lib/searchlogic/search/scopes.rb:
--------------------------------------------------------------------------------
1 | module Searchlogic
2 | class Search
3 | module Scopes
4 | private
5 | def scope_name(condition_name)
6 | condition_name && normalize_scope_name(condition_name)
7 | end
8 |
9 | def scope?(scope_name)
10 | klass.scopes.key?(scope_name) || klass.condition?(scope_name)
11 | end
12 |
13 | def scope_options(name)
14 | 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
15 | klass.named_scope_options(name)
16 | end
17 | end
18 | end
19 | end
--------------------------------------------------------------------------------
/lib/searchlogic/search/to_yaml.rb:
--------------------------------------------------------------------------------
1 | module Searchlogic
2 | class Search
3 | module ToYaml
4 | def self.included(klass)
5 | klass.class_eval do
6 | yaml_as "tag:ruby.yaml.org,2002:class"
7 | include InstanceMethods
8 | end
9 | end
10 |
11 | module InstanceMethods
12 | def to_yaml( opts = {} )
13 | YAML::quick_emit( self, opts ) do |out|
14 | out.map("tag:ruby.yaml.org,2002:object:Searchlogic::Search") do |map|
15 | map.add('class_name', klass.name)
16 | map.add('current_scope', current_scope)
17 | map.add('conditions', conditions)
18 | end
19 | end
20 | end
21 |
22 | def yaml_initialize(taguri, attributes = {})
23 | self.klass = attributes["class_name"].constantize
24 | self.current_scope = attributes["current_scope"]
25 | @conditions ||= {}
26 | self.conditions = attributes["conditions"]
27 | end
28 | end
29 | end
30 | end
31 | end
--------------------------------------------------------------------------------
/lib/searchlogic/search/unknown_condition_error.rb:
--------------------------------------------------------------------------------
1 | module Searchlogic
2 | class Search
3 | # Is an invalid condition is used this error will be raised. Ex:
4 | #
5 | # User.search(:unkown => true)
6 | #
7 | # Where unknown is not a valid named scope for the User model.
8 | class UnknownConditionError < StandardError
9 | def initialize(condition)
10 | msg = "The #{condition} is not a valid condition. You may only use conditions that map to a named scope"
11 | super(msg)
12 | end
13 | end
14 | end
15 | end
--------------------------------------------------------------------------------
/lib/searchlogic/version.rb:
--------------------------------------------------------------------------------
1 | module Searchlogic
2 | VERSION = "2.5.19"
3 | end
4 |
--------------------------------------------------------------------------------
/rails/init.rb:
--------------------------------------------------------------------------------
1 | require "searchlogic"
--------------------------------------------------------------------------------
/searchlogic.gemspec:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | $:.push File.expand_path("../lib", __FILE__)
3 | require File.expand_path('../lib/searchlogic/version', __FILE__)
4 |
5 | Gem::Specification.new do |s|
6 | s.name = "searchlogic"
7 | s.version = Searchlogic::VERSION
8 | s.platform = Gem::Platform::RUBY
9 | s.authors = ["Ben Johnson"]
10 | s.email = ["bjohnson@binarylogic.com"]
11 | s.homepage = "http://github.com/binarylogic/searchlogic"
12 | s.summary = %q{Searchlogic makes using ActiveRecord named scopes easier and less repetitive.}
13 | s.description = %q{Searchlogic makes using ActiveRecord named scopes easier and less repetitive.}
14 |
15 | s.add_dependency 'activerecord', '~> 2.3.12'
16 | s.add_dependency 'activesupport', '~> 2.3.12'
17 | s.add_development_dependency 'rspec', '~> 1.3.1'
18 | s.add_development_dependency 'timecop', '~> 0.5.9.1'
19 | s.add_development_dependency 'sqlite3', '~> 1.3.8'
20 | s.add_development_dependency 'appraisal', '0.4.1'
21 | s.add_development_dependency 'pry', '>= 0'
22 |
23 |
24 | s.files = `git ls-files`.split("\n")
25 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
26 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
27 | s.require_paths = ["lib"]
28 | end
29 |
--------------------------------------------------------------------------------
/spec/searchlogic/active_record/association_proxy_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "Searchlogic::ActiveRecord::AssociationProxy" do
4 | it "should call location conditions" do
5 | company = Company.create
6 | user = company.users.create(:username => "bjohnson")
7 | company.users.send(:username_like, "bjohnson").should == [user]
8 | end
9 |
10 | it "should call ordering conditions" do
11 | company = Company.create
12 | user = company.users.create(:username => "bjohnson")
13 | company.users.send(:ascend_by_username).should == [user]
14 | end
15 |
16 | it "should call 'or' conditions" do
17 | company = Company.create
18 | user = company.users.create(:username => "bjohnson")
19 | company.users.send(:username_or_some_type_id_like, "bjohnson").should == [user]
20 | end
21 |
22 | it "should ignore belongs_to associations" do
23 | user = User.create(:male => true)
24 | cart = user.carts.create
25 | cart.user.send("male").should == true
26 | end
27 | end
28 |
29 |
--------------------------------------------------------------------------------
/spec/searchlogic/active_record/consistency_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Searchlogic::ActiveRecord::Consistency do
4 | it "should merge joins with consistent conditions" do
5 | user_group = UserGroup.create
6 | user_group.users.user_groups_name_like("name").user_groups_id_gt(10).scope(:find)[:joins].should == [
7 | "INNER JOIN \"user_groups_users\" ON \"user_groups_users\".user_id = \"users\".id",
8 | "INNER JOIN \"user_groups\" ON \"user_groups\".id = \"user_groups_users\".user_group_id"
9 | ]
10 | end
11 |
12 | it "should respect parenthesis when reordering conditions" do
13 | joins = [
14 | "INNER JOIN \"table\" ON (\"b\".user_id = \"a\".id)",
15 | "INNER JOIN \"table\" ON (\"b\".id = \"a\".user_group_id)"
16 | ]
17 | ActiveRecord::Base.send(:merge_joins, joins).should == [
18 | "INNER JOIN \"table\" ON \"a\".id = \"b\".user_id",
19 | "INNER JOIN \"table\" ON \"a\".user_group_id = \"b\".id"
20 | ]
21 | end
22 |
23 | it "shuold not convert joins to strings when delegating via associations" do
24 | User.alias_scope :has_id_gt, lambda { User.id_gt(10).has_name.orders_id_gt(10) }
25 | User.alias_scope :has_name, lambda { User.orders_created_at_after(Time.now).name_equals("ben").username_equals("ben") }
26 | Company.users_has_id_gt.proxy_options[:joins].should == {:users=>[:orders]}
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/spec/searchlogic/core_ext/object_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Searchlogic::CoreExt::Object do
4 | it "should accept and pass the argument to the searchlogic_options" do
5 | proc = searchlogic_lambda(:integer, :test => :value) {}
6 | proc.searchlogic_options[:type].should == :integer
7 | proc.searchlogic_options[:test].should == :value
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/spec/searchlogic/core_ext/proc_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Searchlogic::CoreExt::Proc do
4 | it "should have a searchlogic_options accessor" do
5 | p = Proc.new {}
6 | p.searchlogic_options[:type] = :integer
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/spec/searchlogic/named_scopes/alias_scope_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Searchlogic::NamedScopes::AliasScope do
4 | before(:each) do
5 | User.alias_scope :username_has, lambda { |value| User.username_like(value) }
6 | end
7 |
8 | it "should allow alias scopes" do
9 | User.create(:username => "bjohnson")
10 | User.create(:username => "thunt")
11 | User.username_has("bjohnson").all.should == User.find_all_by_username("bjohnson")
12 | end
13 |
14 | it "should allow alias scopes with symbols" do
15 | User.alias_scope :login_has, :username_has
16 | User.create(:username => "bjohnson")
17 | User.create(:username => "thunt")
18 | User.login_has("bjohnson").all.should == User.find_all_by_username("bjohnson")
19 | end
20 |
21 | it "should allow alias scopes from the search object" do
22 | search = User.search
23 | search.username_has = "bjohnson"
24 | search.username_has.should == "bjohnson"
25 | end
26 |
27 | it "should inherit alias scopes from superclasses" do
28 | Class.new(User).condition?("username_has").should be_true
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/spec/searchlogic/named_scopes/association_conditions_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Searchlogic::NamedScopes::AssociationConditions do
4 | it "should create a named scope" do
5 | Company.users_username_like("bjohnson").proxy_options.should == User.username_like("bjohnson").proxy_options.merge(:joins => :users)
6 | end
7 |
8 | it "should create a deep named scope" do
9 | Company.users_orders_total_greater_than(10).proxy_options.should == Order.total_greater_than(10).proxy_options.merge(:joins => {:users => :orders})
10 | end
11 |
12 | it "should allow the use of foreign pre-existing named scopes" do
13 | User.named_scope :uname, lambda { |value| {:conditions => ["users.username = ?", value]} }
14 | Company.users_uname("bjohnson").proxy_options.should == User.uname("bjohnson").proxy_options.merge(:joins => :users)
15 | end
16 |
17 | it "should allow the use of deep foreign pre-existing named scopes" do
18 | pending
19 | Order.named_scope :big_id, :conditions => "orders.id > 100"
20 | Company.users_orders_big_id.proxy_options.should == Order.big_id.proxy_options.merge(:joins => {:users => :orders})
21 | end
22 |
23 | it "should allow the use of foreign pre-existing alias scopes" do
24 | User.alias_scope :username_has, lambda { |value| User.username_like(value) }
25 | Company.users_username_has("bjohnson").proxy_options.should == User.username_has("bjohnson").proxy_options.merge(:joins => :users)
26 | end
27 |
28 | it "should not raise errors for scopes that don't return anything" do
29 | User.alias_scope :blank_scope, lambda { |value| }
30 | Company.users_blank_scope("bjohnson").proxy_options.should == {:joins => :users}
31 | end
32 |
33 | it "should ignore polymorphic associations" do
34 | lambda { Fee.owner_created_at_gt(Time.now) }.should raise_error(NoMethodError)
35 | end
36 |
37 | it "should not allow named scopes on non existent association columns" do
38 | lambda { User.users_whatever_like("bjohnson") }.should raise_error(NoMethodError)
39 | end
40 |
41 | it "should not allow named scopes on non existent deep association columns" do
42 | lambda { User.users_orders_whatever_like("bjohnson") }.should raise_error(NoMethodError)
43 | end
44 |
45 | it "should allow named scopes to be called multiple times and reflect the value passed" do
46 | Company.users_username_like("bjohnson").proxy_options.should == User.username_like("bjohnson").proxy_options.merge(:joins => :users)
47 | Company.users_username_like("thunt").proxy_options.should == User.username_like("thunt").proxy_options.merge(:joins => :users)
48 | end
49 |
50 | it "should allow deep named scopes to be called multiple times and reflect the value passed" do
51 | Company.users_orders_total_greater_than(10).proxy_options.should == Order.total_greater_than(10).proxy_options.merge(:joins => {:users => :orders})
52 | Company.users_orders_total_greater_than(20).proxy_options.should == Order.total_greater_than(20).proxy_options.merge(:joins => {:users => :orders})
53 | end
54 |
55 | it "should have an arity of 1 if the underlying scope has an arity of 1" do
56 | Company.users_orders_total_greater_than(10)
57 | Company.named_scope_arity("users_orders_total_greater_than").should == Order.named_scope_arity("total_greater_than")
58 | end
59 |
60 | it "should have an arity of nil if the underlying scope has an arity of nil" do
61 | Company.users_orders_total_null
62 | Company.named_scope_arity("users_orders_total_null").should == Order.named_scope_arity("total_null")
63 | end
64 |
65 | it "should have an arity of -1 if the underlying scope has an arity of -1" do
66 | Company.users_id_equals_any
67 | Company.named_scope_arity("users_id_equals_any").should == User.named_scope_arity("id_equals_any")
68 | end
69 |
70 | it "should allow aliases" do
71 | Company.users_username_contains("bjohnson").proxy_options.should == User.username_contains("bjohnson").proxy_options.merge(:joins => :users)
72 | end
73 |
74 | it "should allow deep aliases" do
75 | Company.users_orders_total_gt(10).proxy_options.should == Order.total_gt(10).proxy_options.merge(:joins => {:users => :orders})
76 | end
77 |
78 | it "should copy over the named scope options" do
79 | Order.user_whatever_at_equals(1)
80 | Order.named_scope_options(:user_whatever_at_equals).searchlogic_options[:skip_conversion].should == true
81 | end
82 |
83 | it "should include optional associations" do
84 | pending # this is a problem with using inner joins and left outer joins
85 | Company.create
86 | company = Company.create
87 | user = company.users.create
88 | order = user.orders.create(:total => 20, :taxes => 3)
89 | Company.ascend_by_users_orders_total.all.should == Company.all
90 | end
91 |
92 | it "should implement exclusive scoping" do
93 | scope = Company.users_company_name_like("name").users_company_description_like("description")
94 | scope.scope(:find)[:joins].should == [
95 | "INNER JOIN \"users\" ON companies.id = users.company_id",
96 | "INNER JOIN \"companies\" companies_users ON \"companies_users\".id = \"users\".company_id"
97 | ]
98 | lambda { scope.all }.should_not raise_error
99 | end
100 |
101 | it "should not create the same join twice" do
102 | scope = Company.users_orders_total_gt(10).users_orders_taxes_lt(5).ascend_by_users_orders_total
103 | scope.scope(:find)[:joins].should == [
104 | "INNER JOIN \"users\" ON companies.id = users.company_id",
105 | "INNER JOIN \"orders\" ON orders.user_id = users.id"
106 | ]
107 | lambda { scope.count }.should_not raise_error
108 | end
109 |
110 | it "should not create the same join twice when traveling through the duplicate join" do
111 | scope = Company.users_username_like("bjohnson").users_orders_total_gt(100)
112 | scope.scope(:find)[:joins].should == [
113 | "INNER JOIN \"users\" ON companies.id = users.company_id",
114 | "INNER JOIN \"orders\" ON orders.user_id = users.id"
115 | ]
116 | lambda { scope.count }.should_not raise_error
117 | end
118 |
119 | it "should not create the same join twice when traveling through the deep duplicate join" do
120 | scope = Company.users_orders_total_gt(100).users_orders_line_items_price_gt(20)
121 | scope.scope(:find)[:joins].should == [
122 | "INNER JOIN \"users\" ON companies.id = users.company_id",
123 | "INNER JOIN \"orders\" ON orders.user_id = users.id",
124 | "INNER JOIN \"line_items\" ON line_items.order_id = orders.id"
125 | ]
126 | lambda { scope.all }.should_not raise_error
127 | end
128 |
129 | it "should allow the use of :include when a join was created" do
130 | company = Company.create
131 | user = company.users.create
132 | order = user.orders.create(:total => 20, :taxes => 3)
133 | Company.users_orders_total_gt(10).users_orders_taxes_lt(5).ascend_by_users_orders_total.all(:include => :users).should == Company.all
134 | end
135 |
136 | it "should allow the use of deep :include when a join was created" do
137 | company = Company.create
138 | user = company.users.create
139 | order = user.orders.create(:total => 20, :taxes => 3)
140 | Company.users_orders_total_gt(10).users_orders_taxes_lt(5).ascend_by_users_orders_total.all(:include => {:users => :orders}).should == Company.all
141 | end
142 |
143 | it "should allow the use of :include when traveling through the duplicate join" do
144 | company = Company.create
145 | user = company.users.create(:username => "bjohnson")
146 | order = user.orders.create(:total => 20, :taxes => 3)
147 | Company.users_username_like("bjohnson").users_orders_taxes_lt(5).ascend_by_users_orders_total.all(:include => :users).should == Company.all
148 | end
149 |
150 | it "should allow the use of deep :include when traveling through the duplicate join" do
151 | company = Company.create
152 | user = company.users.create(:username => "bjohnson")
153 | order = user.orders.create(:total => 20, :taxes => 3)
154 | Company.users_orders_taxes_lt(50).ascend_by_users_orders_total.all(:include => {:users => :orders}).should == Company.all
155 | end
156 |
157 | it "should automatically add string joins if the association condition is using strings" do
158 | User.named_scope(:orders_big_id, :joins => User.inner_joins(:orders))
159 | Company.users_orders_big_id.proxy_options.should == {:joins=>[" INNER JOIN \"users\" ON users.company_id = companies.id ", " INNER JOIN \"orders\" ON orders.user_id = users.id "]}
160 | end
161 |
162 | it "should order the join statements ascending by the fieldnames so that we don't get double joins where the only difference is that the order of the fields is different" do
163 | company = Company.create
164 | user = company.users.create(:company_id => company.id)
165 | company.users.company_id_eq(company.id).should == [user]
166 | end
167 |
168 | it "should sanitize the scope on a foreign model instead of passing the raw options back to the original" do
169 | Company.named_scope(:users_count_10, :conditions => {:users_count => 10})
170 | User.company_users_count_10.proxy_options.should == {:conditions => "\"companies\".\"users_count\" = 10", :joins => :company}
171 | end
172 |
173 | it "should delegate to polymorphic relationships" do
174 | Audit.auditable_user_type_name_like("ben").proxy_options.should == {
175 | :conditions => ["users.name LIKE ?", "%ben%"],
176 | :joins => "INNER JOIN \"users\" ON \"users\".id = \"audits\".auditable_id AND \"audits\".auditable_type = 'User'"
177 | }
178 | end
179 |
180 | it "should delegate to polymorphic relationships (with a lazy split on _type_)" do
181 | Audit.auditable_user_type_some_type_id_like("ben").proxy_options.should == {
182 | :conditions => ["users.some_type_id LIKE ?", "%ben%"],
183 | :joins => "INNER JOIN \"users\" ON \"users\".id = \"audits\".auditable_id AND \"audits\".auditable_type = 'User'"
184 | }
185 | end
186 |
187 | it "should deep delegate to polymorphic relationships" do
188 | Audit.auditable_user_type_company_name_like("company").proxy_options.should == {
189 | :conditions => ["companies.name LIKE ?", "%company%"],
190 | :joins => ["INNER JOIN \"users\" ON \"users\".id = \"audits\".auditable_id AND \"audits\".auditable_type = 'User'", " INNER JOIN \"companies\" ON \"companies\".id = \"users\".company_id "]
191 | }
192 | end
193 |
194 | it "should allow any on a has_many relationship" do
195 | company1 = Company.create
196 | user1 = company1.users.create
197 | company2 = Company.create
198 | user2 = company2.users.create
199 | user3 = company2.users.create
200 |
201 | Company.users_id_equals_any([user2.id, user3.id]).all(:select => "DISTINCT companies.*").should == [company2]
202 | end
203 |
204 | it "should allow dynamic scope generation on associations without losing association scope options" do
205 | user = User.create
206 | Order.create :user => user, :shipped_on => Time.now
207 | Order.create :shipped_on => Time.now
208 | Order.named_scope :shipped_on_not_null, :conditions => ['shipped_on is not null']
209 | user.orders.count.should == 1
210 | user.orders.shipped_on_not_null.shipped_on_greater_than(2.days.ago).count.should == 1
211 | end
212 |
213 | it "should allow chained dynamic scopes without losing association scope conditions" do
214 | user = User.create
215 | order1 = Order.create :user => user, :shipped_on => Time.now, :total => 2
216 | order2 = Order.create :shipped_on => Time.now, :total => 2
217 | user.orders.id_equals(order1.id).count.should == 1
218 | user.orders.id_equals(order1.id).total_equals(2).count.should == 1
219 | end
220 |
221 | it "shouldn't cache the lambda of a named_scope for a chained association" do
222 | user = User.create
223 | Order.create :user => user, :shipped_on => Time.current
224 |
225 | # create the named_scope and use it through a chained association
226 | Order.named_scope :shipped, lambda { {:conditions => ["shipped_on <= ?", Time.current]} }
227 | User.orders_shipped
228 |
229 | # simulate a day passing after the chained scope is first used
230 | Timecop.travel(Date.tomorrow)
231 |
232 | # make a new object that is active on our simulated day
233 | Order.create :user => user, :shipped_on => Time.current
234 |
235 | # we have 2 orders and both were shipped on or before the current time,
236 | # so both should be included in the scope
237 | user.orders.count.should == 2
238 | User.orders_shipped.count.should == 2
239 | end
240 |
241 | it "should allow Marshal.dump on objects that only have polymorphic associations where a polymorphic association is loaded" do
242 | audit = Audit.create
243 | audit.auditable = User.create
244 | lambda { Marshal.dump(audit) }.should_not raise_error
245 | end
246 | end
247 |
--------------------------------------------------------------------------------
/spec/searchlogic/named_scopes/association_ordering_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Searchlogic::NamedScopes::Ordering do
4 | it "should allow ascending" do
5 | Company.ascend_by_users_username.proxy_options.should == User.ascend_by_username.proxy_options.merge(:joins => :users)
6 | end
7 |
8 | it "should allow descending" do
9 | Company.descend_by_users_username.proxy_options.should == User.descend_by_username.proxy_options.merge(:joins => :users)
10 | end
11 |
12 | it "should allow deep ascending" do
13 | Company.ascend_by_users_orders_total.proxy_options.should == Order.ascend_by_total.proxy_options.merge(:joins => {:users => :orders})
14 | end
15 |
16 | it "should allow deep descending" do
17 | Company.descend_by_users_orders_total.proxy_options.should == Order.descend_by_total.proxy_options.merge(:joins => {:users => :orders})
18 | end
19 |
20 | it "should ascend with a belongs to" do
21 | User.ascend_by_company_name.proxy_options.should == Company.ascend_by_name.proxy_options.merge(:joins => :company)
22 | end
23 |
24 | it "should work through #order" do
25 | Company.order('ascend_by_users_username').proxy_options.should == Company.ascend_by_users_username.proxy_options
26 | end
27 |
28 | it "should ascend with a polymorphic belongs to" do
29 | Audit.descend_by_auditable_user_type_username.proxy_options.should ==
30 | User.descend_by_username.proxy_options.merge(
31 | :joins => "INNER JOIN \"users\" ON \"users\".id = \"audits\".auditable_id AND \"audits\".auditable_type = 'User'"
32 | )
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/spec/searchlogic/named_scopes/column_conditions_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Searchlogic::NamedScopes::ColumnConditions do
4 | it "should be dynamically created and then cached" do
5 | User.scopes.key?(:age_less_than).should == false
6 | User.age_less_than(5)
7 | User.scopes.key?(:age_less_than).should == true
8 | end
9 |
10 | it "should respond to the scope" do
11 | User.should respond_to(:age_less_than)
12 | end
13 |
14 | it "should not allow conditions on non columns" do
15 | lambda { User.whatever_equals(2) }.should raise_error(NoMethodError)
16 | end
17 |
18 | context "comparison conditions" do
19 | it "should have equals" do
20 | (5..7).each { |age| User.create(:age => age) }
21 | nil_user = User.create
22 | User.age_equals(6).all.should == User.find_all_by_age(6)
23 | User.age_equals(nil).all.should == User.find_all_by_age(nil)
24 | end
25 |
26 | it "should have does not equal" do
27 | (5..7).each { |age| User.create(:age => age) }
28 | User.age_does_not_equal(6).all.should == User.find_all_by_age([5,7])
29 |
30 | User.create!(:age => nil)
31 | User.age_does_not_equal(nil).all.size.should == 3
32 | end
33 |
34 | it "should have less than" do
35 | (5..7).each { |age| User.create(:age => age) }
36 | User.age_less_than(6).all.should == User.find_all_by_age(5)
37 | end
38 |
39 | it "should have less than or equal to" do
40 | (5..7).each { |age| User.create(:age => age) }
41 | User.age_less_than_or_equal_to(6).all.should == User.find_all_by_age([5, 6])
42 | end
43 |
44 | it "should have greater than" do
45 | (5..7).each { |age| User.create(:age => age) }
46 | User.age_greater_than(6).all.should == User.find_all_by_age(7)
47 | end
48 |
49 | it "should have greater than or equal to" do
50 | (5..7).each { |age| User.create(:age => age) }
51 | User.age_greater_than_or_equal_to(6).all.should == User.find_all_by_age([6, 7])
52 | end
53 | end
54 |
55 | context "wildcard conditions" do
56 | it "should have like" do
57 | %w(bjohnson thunt).each { |username| User.create(:username => username) }
58 | User.username_like("john").all.should == User.find_all_by_username("bjohnson")
59 | end
60 |
61 | it "should have not like" do
62 | %w(bjohnson thunt).each { |username| User.create(:username => username) }
63 | User.username_not_like("john").all.should == User.find_all_by_username("thunt")
64 | end
65 |
66 | it "should have begins with" do
67 | %w(bjohnson thunt).each { |username| User.create(:username => username) }
68 | User.username_begins_with("bj").all.should == User.find_all_by_username("bjohnson")
69 | end
70 |
71 | it "should have not begin with" do
72 | %w(bjohnson thunt).each { |username| User.create(:username => username) }
73 | User.username_not_begin_with("bj").all.should == User.find_all_by_username("thunt")
74 | end
75 |
76 | it "should have ends with" do
77 | %w(bjohnson thunt).each { |username| User.create(:username => username) }
78 | User.username_ends_with("son").all.should == User.find_all_by_username("bjohnson")
79 | end
80 |
81 | it "should have not end with" do
82 | %w(bjohnson thunt).each { |username| User.create(:username => username) }
83 | User.username_not_end_with("son").all.should == User.find_all_by_username("thunt")
84 | end
85 | end
86 |
87 | context "boolean conditions" do
88 | it "should have scopes for boolean columns" do
89 | female = User.create(:male => false)
90 | male = User.create(:male => true)
91 | User.male.all.should == [male]
92 | User.not_male.all.should == [female]
93 | end
94 |
95 | it "should have null" do
96 | ["bjohnson", nil].each { |username| User.create(:username => username) }
97 | User.username_null.all.should == User.find_all_by_username(nil)
98 | end
99 |
100 | it "should have not null" do
101 | ["bjohnson", nil].each { |username| User.create(:username => username) }
102 | User.username_not_null.all.should == User.find_all_by_username("bjohnson")
103 | end
104 |
105 | it "should have empty" do
106 | ["bjohnson", ""].each { |username| User.create(:username => username) }
107 | User.username_empty.all.should == User.find_all_by_username("")
108 | end
109 |
110 | it "should have blank" do
111 | ["bjohnson", "", nil].each { |username| User.create(:username => username) }
112 | User.username_blank.all.should == [User.find_by_username(""), User.find_by_username(nil)]
113 | end
114 |
115 | it "should have not blank" do
116 | ["bjohnson", "", nil].each { |username| User.create(:username => username) }
117 | User.username_not_blank.all.should == User.find_all_by_username("bjohnson")
118 | end
119 | end
120 |
121 | context "any and all conditions" do
122 | it "should do nothing if no arguments are passed" do
123 | User.username_equals_any.proxy_options.should == {}
124 | end
125 |
126 | it "should treat an array and multiple arguments the same" do
127 | %w(bjohnson thunt dgainor).each { |username| User.create(:username => username) }
128 | User.username_like_any("bjohnson", "thunt").should == User.username_like_any(["bjohnson", "thunt"])
129 | end
130 |
131 | it "should have equals any" do
132 | %w(bjohnson thunt dgainor).each { |username| User.create(:username => username) }
133 | User.username_equals_any("bjohnson", "thunt").all.should == User.find_all_by_username(["bjohnson", "thunt"])
134 | end
135 |
136 | # PostgreSQL does not allow null in "in" statements
137 | it "should have equals any and handle nils" do
138 | %w(bjohnson thunt dgainor).each { |username| User.create(:username => username) }
139 | User.username_equals_any("bjohnson", "thunt", nil).proxy_options.should == {:conditions=>["users.username IN (?) OR users.username IS ?", ["bjohnson", "thunt"], nil]}
140 | end
141 |
142 | it "should have equals all" do
143 | %w(bjohnson thunt dainor).each { |username| User.create(:username => username) }
144 | User.username_equals_all("bjohnson", "thunt").all.should == []
145 | end
146 |
147 | it "should have does not equal any" do
148 | %w(bjohnson thunt dgainor).each { |username| User.create(:username => username) }
149 | User.username_does_not_equal_any("bjohnson", "thunt").all.should == User.find_all_by_username(["bjohnson", "thunt", "dgainor"])
150 | end
151 |
152 | it "should have does not equal all" do
153 | %w(bjohnson thunt dgainor).each { |username| User.create(:username => username) }
154 | User.username_does_not_equal_all("bjohnson", "thunt").all.should == User.find_all_by_username("dgainor")
155 | end
156 |
157 | it "should have less than any" do
158 | (5..7).each { |age| User.create(:age => age) }
159 | User.age_less_than_any(7,6).all.should == User.find_all_by_age([5, 6])
160 | end
161 |
162 | it "should have less than all" do
163 | (5..7).each { |age| User.create(:age => age) }
164 | User.age_less_than_all(7,6).all.should == User.find_all_by_age(5)
165 | end
166 |
167 | it "should have less than or equal to any" do
168 | (5..7).each { |age| User.create(:age => age) }
169 | User.age_less_than_or_equal_to_any(7,6).all.should == User.find_all_by_age([5, 6, 7])
170 | end
171 |
172 | it "should have less than or equal to all" do
173 | (5..7).each { |age| User.create(:age => age) }
174 | User.age_less_than_or_equal_to_all(7,6).all.should == User.find_all_by_age([5, 6])
175 | end
176 |
177 | it "should have less than any" do
178 | (5..7).each { |age| User.create(:age => age) }
179 | User.age_greater_than_any(5,6).all.should == User.find_all_by_age([6, 7])
180 | end
181 |
182 | it "should have greater than all" do
183 | (5..7).each { |age| User.create(:age => age) }
184 | User.age_greater_than_all(5,6).all.should == User.find_all_by_age(7)
185 | end
186 |
187 | it "should have greater than or equal to any" do
188 | (5..7).each { |age| User.create(:age => age) }
189 | User.age_greater_than_or_equal_to_any(5,6).all.should == User.find_all_by_age([5, 6, 7])
190 | end
191 |
192 | it "should have greater than or equal to all" do
193 | (5..7).each { |age| User.create(:age => age) }
194 | User.age_greater_than_or_equal_to_all(5,6).all.should == User.find_all_by_age([6, 7])
195 | end
196 |
197 | it "should have like all" do
198 | %w(bjohnson thunt dgainor).each { |username| User.create(:username => username) }
199 | User.username_like_all("bjohnson", "thunt").all.should == []
200 | User.username_like_all("n", "o").all.should == User.find_all_by_username(["bjohnson", "dgainor"])
201 | end
202 |
203 | it "should have like any" do
204 | %w(bjohnson thunt dgainor).each { |username| User.create(:username => username) }
205 | User.username_like_any("bjohnson", "thunt").all.should == User.find_all_by_username(["bjohnson", "thunt"])
206 | end
207 |
208 | it "should have begins with all" do
209 | %w(bjohnson thunt dgainor).each { |username| User.create(:username => username) }
210 | User.username_begins_with_all("bjohnson", "thunt").all.should == []
211 | end
212 |
213 | it "should have begins with any" do
214 | %w(bjohnson thunt dgainor).each { |username| User.create(:username => username) }
215 | User.username_begins_with_any("bj", "th").all.should == User.find_all_by_username(["bjohnson", "thunt"])
216 | end
217 |
218 | it "should have ends with all" do
219 | %w(bjohnson thunt dgainor).each { |username| User.create(:username => username) }
220 | User.username_ends_with_all("n", "r").all.should == []
221 | end
222 |
223 | it "should have ends with any" do
224 | %w(bjohnson thunt dgainor).each { |username| User.create(:username => username) }
225 | User.username_ends_with_any("n", "r").all.should == User.find_all_by_username(["bjohnson", "dgainor"])
226 | end
227 | end
228 |
229 | context "alias conditions" do
230 | it "should have is" do
231 | User.age_is(5).proxy_options.should == User.age_equals(5).proxy_options
232 | end
233 |
234 | it "should have eq" do
235 | User.age_eq(5).proxy_options.should == User.age_equals(5).proxy_options
236 | end
237 |
238 | it "should have not_equal_to" do
239 | User.age_not_equal_to(5).proxy_options.should == User.age_does_not_equal(5).proxy_options
240 | end
241 |
242 | it "should have is_not" do
243 | User.age_is_not(5).proxy_options.should == User.age_does_not_equal(5).proxy_options
244 | end
245 |
246 | it "should have not" do
247 | User.age_not(5).proxy_options.should == User.age_does_not_equal(5).proxy_options
248 | end
249 |
250 | it "should have ne" do
251 | User.age_ne(5).proxy_options.should == User.age_does_not_equal(5).proxy_options
252 | end
253 |
254 | it "should have lt" do
255 | User.age_lt(5).proxy_options.should == User.age_less_than(5).proxy_options
256 | end
257 |
258 | it "should have lte" do
259 | User.age_lte(5).proxy_options.should == User.age_less_than_or_equal_to(5).proxy_options
260 | end
261 |
262 | it "should have gt" do
263 | User.age_gt(5).proxy_options.should == User.age_greater_than(5).proxy_options
264 | end
265 |
266 | it "should have gte" do
267 | User.age_gte(5).proxy_options.should == User.age_greater_than_or_equal_to(5).proxy_options
268 | end
269 |
270 | it "should have contains" do
271 | User.username_contains(5).proxy_options.should == User.username_like(5).proxy_options
272 | end
273 |
274 | it "should have contains" do
275 | User.username_includes(5).proxy_options.should == User.username_like(5).proxy_options
276 | end
277 |
278 | it "should have bw" do
279 | User.username_bw(5).proxy_options.should == User.username_begins_with(5).proxy_options
280 | end
281 |
282 | it "should have ew" do
283 | User.username_ew(5).proxy_options.should == User.username_ends_with(5).proxy_options
284 | end
285 |
286 | it "should have nil" do
287 | User.username_nil.proxy_options.should == User.username_nil.proxy_options
288 | end
289 | end
290 |
291 | context "group conditions" do
292 | it "should have in" do
293 | (5..7).each { |age| User.create(:age => age) }
294 | User.age_in([5,6]).all.should == User.find(:all, :conditions => ["users.age IN (?)", [5, 6]])
295 | end
296 |
297 | it "should have not_in" do
298 | (5..7).each { |age| User.create(:age => age) }
299 | User.age_not_in([5,6]).all.should == User.find(:all, :conditions => ["users.age NOT IN (?)", [5, 6]])
300 | end
301 | end
302 |
303 | context "searchlogic lambda" do
304 | it "should be a string" do
305 | User.username_like("test")
306 | User.named_scope_options(:username_like).searchlogic_options[:type].should == :string
307 | end
308 |
309 | it "should be an integer" do
310 | User.id_gt(10)
311 | User.named_scope_options(:id_gt).searchlogic_options[:type].should == :integer
312 | end
313 |
314 | it "should be a float" do
315 | Order.total_gt(10)
316 | Order.named_scope_options(:total_gt).searchlogic_options[:type].should == :float
317 | end
318 | end
319 |
320 | it "should have priorty to columns over conflicting association conditions" do
321 | Company.users_count_gt(10)
322 | User.create
323 | User.company_id_null.count.should == 1
324 | User.company_id_not_null.count.should == 0
325 | end
326 |
327 | it "should fix bug for issue 26" do
328 | count1 = User.id_ne(10).username_not_like("root").count
329 | count2 = User.id_ne(10).username_not_like("root").count
330 | count1.should == count2
331 | end
332 |
333 | it "should produce left outer joins" do
334 | User.left_outer_joins(:orders).should == [" LEFT OUTER JOIN \"orders\" ON orders.user_id = users.id "]
335 | end
336 | end
337 |
--------------------------------------------------------------------------------
/spec/searchlogic/named_scopes/or_conditions_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Searchlogic::NamedScopes::OrConditions do
4 | it "should define a scope by the exact same name as requested by the code" do
5 | User.name_or_username_like('Test')
6 | User.respond_to?(:name_or_username_like).should be_true
7 | end
8 |
9 | it "should match username or name" do
10 | User.username_or_name_like("ben").proxy_options.should == {:conditions => "(users.username LIKE '%ben%') OR (users.name LIKE '%ben%')"}
11 | end
12 |
13 | it "should use the specified condition" do
14 | User.username_begins_with_or_name_like("ben").proxy_options.should == {:conditions => "(users.username LIKE 'ben%') OR (users.name LIKE '%ben%')"}
15 | end
16 |
17 | it "should use the last specified condition" do
18 | User.username_or_name_like_or_id_or_age_lt(10).proxy_options.should == {:conditions => "(users.username LIKE '%10%') OR (users.name LIKE '%10%') OR (users.id < 10) OR (users.age < 10)"}
19 | end
20 |
21 | it "should raise an error on unknown conditions" do
22 | lambda { User.usernme_begins_with_or_name_like("ben") }.should raise_error(Searchlogic::NamedScopes::OrConditions::UnknownConditionError)
23 | end
24 |
25 | it "should work well with _or_equal_to" do
26 | User.id_less_than_or_equal_to_or_age_gt(10).proxy_options.should == {:conditions => "(users.id <= 10) OR (users.age > 10)"}
27 | end
28 |
29 | it "should work well with _or_equal_to_any" do
30 | User.id_less_than_or_equal_to_all_or_age_gt(10).proxy_options.should == {:conditions => "(users.id <= 10) OR (users.age > 10)"}
31 | end
32 |
33 | it "should work well with _or_equal_to_all" do
34 | User.id_less_than_or_equal_to_any_or_age_gt(10).proxy_options.should == {:conditions => "(users.id <= 10) OR (users.age > 10)"}
35 | end
36 |
37 | it "should play nice with other scopes" do
38 | User.username_begins_with("ben").id_gt(10).age_not_nil.username_or_name_ends_with("ben").scope(:find).should ==
39 | {:conditions => "((users.username LIKE '%ben') OR (users.name LIKE '%ben')) AND ((users.age IS NOT NULL) AND ((users.id > 10) AND (users.username LIKE 'ben%')))"}
40 | end
41 |
42 | it "should work with boolean conditions" do
43 | User.male_or_name_eq("susan").proxy_options.should == {:conditions => %Q{("users"."male" = 't') OR (users.name = 'susan')}}
44 | User.not_male_or_name_eq("susan").proxy_options.should == {:conditions => %Q{("users"."male" = 'f') OR (users.name = 'susan')}}
45 | lambda { User.male_or_name_eq("susan").all }.should_not raise_error
46 | end
47 |
48 | it "should play nice with scopes on associations" do
49 | lambda { User.name_or_company_name_like("ben") }.should_not raise_error(Searchlogic::NamedScopes::OrConditions::NoConditionSpecifiedError)
50 | User.name_or_company_name_like("ben").proxy_options.should == {:joins => ["LEFT OUTER JOIN \"companies\" ON \"companies\".id = \"users\".company_id"], :conditions => "(users.name LIKE '%ben%') OR (companies.name LIKE '%ben%')"}
51 | User.company_name_or_name_like("ben").proxy_options.should == {:joins => ["LEFT OUTER JOIN \"companies\" ON \"companies\".id = \"users\".company_id"], :conditions => "(companies.name LIKE '%ben%') OR (users.name LIKE '%ben%')"}
52 | User.company_name_or_company_description_like("ben").proxy_options.should == {:joins => ["LEFT OUTER JOIN \"companies\" ON \"companies\".id = \"users\".company_id"], :conditions => "(companies.name LIKE '%ben%') OR (companies.description LIKE '%ben%')"}
53 | Cart.user_company_name_or_user_company_name_like("ben").proxy_options.should == {:joins => ["LEFT OUTER JOIN \"users\" ON \"carts\".user_id = \"users\".id", "LEFT OUTER JOIN \"companies\" ON \"companies\".id = \"users\".company_id"], :conditions => "(companies.name LIKE '%ben%') OR (companies.name LIKE '%ben%')"}
54 | end
55 |
56 | it "should raise an error on missing condition" do
57 | lambda { User.id_or_age(123) }.should raise_error(Searchlogic::NamedScopes::OrConditions::NoConditionSpecifiedError)
58 | end
59 |
60 | it "should not get confused by the 'or' in find_or_create_by_* methods" do
61 | User.create(:name => "Fred")
62 | User.find_or_create_by_name("Fred").should be_a_kind_of User
63 | end
64 |
65 | it "should not get confused by the 'or' in compound find_or_create_by_* methods" do
66 | User.create(:name => "Fred", :username => "fredb")
67 | User.find_or_create_by_name_and_username("Fred", "fredb").should be_a_kind_of User
68 | end
69 |
70 | it "should work with User.search(conditions) method" do
71 | User.search(:username_or_name_like => 'ben').proxy_options.should == {:conditions => "(users.username LIKE '%ben%') OR (users.name LIKE '%ben%')"}
72 | end
73 |
74 | it "should convert types properly when used with User.search(conditions) method" do
75 | User.search(:id_or_age_lte => '10').proxy_options.should == {:conditions => "(users.id <= 10) OR (users.age <= 10)"}
76 | end
77 |
78 | it "should converts inner joins to left out joins" do
79 | scopes = []
80 | scopes << User.orders_id_equals(1)
81 | scopes << User.carts_id_equals(1)
82 | User.send(:merge_scopes_with_or, scopes).should == {:conditions=>"(orders.id = 1) OR (carts.id = 1)", :joins=>["LEFT OUTER JOIN \"carts\" ON carts.user_id = users.id", "LEFT OUTER JOIN \"orders\" ON orders.user_id = users.id"]}
83 | end
84 | end
--------------------------------------------------------------------------------
/spec/searchlogic/named_scopes/ordering_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Searchlogic::NamedScopes::Ordering do
4 | it "should have ascending" do
5 | %w(bjohnson thunt).each { |username| User.create(:username => username) }
6 | User.ascend_by_username.all.should == User.all(:order => "username ASC")
7 | end
8 |
9 | it "should have descending" do
10 | %w(bjohnson thunt).each { |username| User.create(:username => username) }
11 | User.descend_by_username.all.should == User.all(:order => "username DESC")
12 | end
13 |
14 | it "should have order" do
15 | User.order("ascend_by_username").proxy_options.should == User.ascend_by_username.proxy_options
16 | end
17 |
18 | it "should have order by custom scope" do
19 | User.column_names.should_not include("custom")
20 | %w(bjohnson thunt fisons).each { |username| User.create(:username => username) }
21 | User.named_scope(:ascend_by_custom, :order => "username ASC, name DESC")
22 | User.order("ascend_by_custom").proxy_options.should == User.ascend_by_custom.proxy_options
23 | end
24 |
25 | it "should have priorty to columns over conflicting association columns" do
26 | Company.ascend_by_users_count
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/spec/searchlogic/search_spec.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.dirname(__FILE__) + "/../spec_helper")
2 |
3 | describe Searchlogic::Search do
4 | describe "Implementation" do
5 | context "#searchlogic" do
6 | it "should create a search proxy" do
7 | User.search(:username => "joe").should be_kind_of(Searchlogic::Search)
8 | end
9 |
10 | it "should create a search proxy using the same class" do
11 | User.search.klass.should == User
12 | end
13 |
14 | it "should pass on the current scope to the proxy" do
15 | company = Company.create
16 | user = company.users.create
17 | search = company.users.search
18 | search.current_scope.should == company.users.scope(:find)
19 | end
20 | end
21 | end
22 |
23 | context "#initialize" do
24 | it "should require a class" do
25 | lambda { Searchlogic::Search.new }.should raise_error(ArgumentError)
26 | end
27 |
28 | it "should set the conditions" do
29 | search = User.search(:username => "bjohnson")
30 | search.conditions.should == {:username => "bjohnson"}
31 | end
32 | end
33 |
34 | context "#clone" do
35 | it "should clone properly" do
36 | company = Company.create
37 | user1 = company.users.create(:age => 5)
38 | user2 = company.users.create(:age => 25)
39 | search1 = company.users.search(:age_gt => 10)
40 | search2 = search1.clone
41 | search2.age_gt = 1
42 | search2.all.should == User.all
43 | search1.all.should == [user2]
44 | end
45 |
46 | it "should clone properly without scope" do
47 | user1 = User.create(:age => 5)
48 | user2 = User.create(:age => 25)
49 | search1 = User.search(:age_gt => 10)
50 | search2 = search1.clone
51 | search2.age_gt = 1
52 | search2.all.should == User.all
53 | search1.all.should == [user2]
54 | end
55 | end
56 |
57 | context "#conditions" do
58 | it "should set the conditions and be accessible individually" do
59 | search = User.search
60 | search.conditions = {:username => "bjohnson"}
61 | search.username.should == "bjohnson"
62 | end
63 |
64 | it "should set the conditions and allow string keys" do
65 | search = User.search
66 | search.conditions = {"username" => "bjohnson"}
67 | search.username.should == "bjohnson"
68 | end
69 |
70 | it "should use custom scopes before normalizing" do
71 | User.create(:username => "bjohnson")
72 | User.named_scope :username, lambda { |value| {:conditions => {:username => value.reverse}} }
73 | search1 = User.search(:username => "bjohnson")
74 | search2 = User.search(:username => "nosnhojb")
75 | search1.count.should == 0
76 | search2.count.should == 1
77 | end
78 |
79 | # We ignore them upon execution. But we still want to accept the condition so that returning the conditions
80 | # preserves the values.
81 | it "should ignore blank values but still return on conditions" do
82 | search = User.search
83 | search.conditions = {"username" => ""}
84 | search.username.should be_nil
85 | search.conditions.should == {:username => ""}
86 | end
87 |
88 | it "should not ignore blank values and should not cast them" do
89 | search = User.search
90 | search.conditions = {"id_equals" => ""}
91 | search.id_equals.should be_nil
92 | search.conditions.should == {:id_equals => ""}
93 | end
94 |
95 | it "should ignore blank values in arrays" do
96 | search = User.search
97 | search.conditions = {"username_equals_any" => [""]}
98 | search.username_equals_any.should be_nil
99 |
100 | search.conditions = {"id_equals_any" => ["", "1"]}
101 | search.id_equals_any.should == [1]
102 | end
103 |
104 | it "should remove duplicate values in arrays" do
105 | search = User.search
106 | search.conditions = {"username_equals_any" => ["dup", "dup"]}
107 | search.username_equals_any.should == ["dup"]
108 | end
109 | end
110 |
111 | context "#compact_conditions" do
112 | it "should remove conditions with blank values" do
113 | search = User.search
114 | search.conditions = {"id_equals" => "", "name_equals" => "Ben"}
115 | search.compact_conditions.should == {:name_equals => "Ben"}
116 | end
117 | end
118 |
119 | context "condition accessors" do
120 | it "should allow setting exact columns individually" do
121 | search = User.search
122 | search.username = "bjohnson"
123 | search.username.should == "bjohnson"
124 | end
125 |
126 | it "should allow setting local column conditions individually" do
127 | search = User.search
128 | search.username_gt = "bjohnson"
129 | search.username_gt.should == "bjohnson"
130 | end
131 |
132 | it "should allow chaining conditions" do
133 | user = User.create(:username => "bjohnson", :age => 20)
134 | User.create(:username => "bjohnson", :age => 5)
135 | search = User.search
136 | search.username_equals("bjohnson").age_gt(10)
137 | search.all.should == [user]
138 | end
139 |
140 | it "should allow setting association conditions" do
141 | search = User.search
142 | search.orders_total_gt = 10
143 | search.orders_total_gt.should == 10
144 | end
145 |
146 | it "should allow setting pre-existing association conditions" do
147 | User.named_scope :uname, lambda { |value| {:conditions => ["users.username = ?", value]} }
148 | search = Company.search
149 | search.users_uname = "bjohnson"
150 | search.users_uname.should == "bjohnson"
151 | end
152 |
153 | it "should allow setting pre-existing association alias conditions" do
154 | User.alias_scope :username_has, lambda { |value| User.username_like(value) }
155 | search = Company.search
156 | search.users_username_has = "bjohnson"
157 | search.users_username_has.should == "bjohnson"
158 | end
159 |
160 | it "should allow using custom conditions" do
161 | User.named_scope(:four_year_olds, { :conditions => { :age => 4 } })
162 | search = User.search
163 | search.four_year_olds = true
164 | search.four_year_olds.should == true
165 | search.proxy_options.should == User.four_year_olds.proxy_options
166 | end
167 |
168 | it "should not merge conflicting conditions into one value" do
169 | # This class should JUST be a proxy. It should not do anything more than that.
170 | # A user would be allowed to call both named scopes if they wanted.
171 | search = User.search
172 | search.username_greater_than = "bjohnson1"
173 | search.username_gt = "bjohnson2"
174 | search.username_greater_than.should == "bjohnson1"
175 | search.username_gt.should == "bjohnson2"
176 | end
177 |
178 | it "should allow setting custom conditions individually with an arity of 0" do
179 | User.named_scope(:four_year_olds, :conditions => {:age => 4})
180 | search = User.search
181 | search.four_year_olds = true
182 | search.four_year_olds.should == true
183 | end
184 |
185 | it "should allow setting custom conditions individually with an arity of 1" do
186 | User.named_scope(:username_should_be, lambda { |u| {:conditions => {:username => u}} })
187 | search = User.search
188 | search.username_should_be = "bjohnson"
189 | search.username_should_be.should == "bjohnson"
190 | end
191 |
192 | it "should not allow setting conditions that are not scopes" do
193 | search = User.search
194 | lambda { search.unknown = true }.should raise_error(Searchlogic::Search::UnknownConditionError)
195 | end
196 |
197 | it "should not allow setting conditions on sensitive methods" do
198 | search = User.search
199 | lambda { search.destroy = true }.should raise_error(Searchlogic::Search::UnknownConditionError)
200 | end
201 |
202 | it "should not use the ruby implementation of the id method" do
203 | search = User.search
204 | search.id.should be_nil
205 | end
206 |
207 | context "type casting" do
208 | it "should be a Boolean given true" do
209 | search = User.search
210 | search.id_nil = true
211 | search.id_nil.should == true
212 | end
213 |
214 | it "should be a Boolean given 'true'" do
215 | search = User.search
216 | search.id_nil = "true"
217 | search.id_nil.should == true
218 | end
219 |
220 | it "should be a Boolean given '1'" do
221 | search = User.search
222 | search.id_nil = "1"
223 | search.id_nil.should == true
224 | end
225 |
226 | it "should be a Boolean given false" do
227 | search = User.search
228 | search.id_nil = false
229 | search.id_nil.should == false
230 | end
231 |
232 | it "should be a Boolean given 'false'" do
233 | search = User.search
234 | search.id_nil = "false"
235 | search.id_nil.should == false
236 | end
237 |
238 | it "should be a Boolean given '0'" do
239 | search = User.search
240 | search.id_nil = "0"
241 | search.id_nil.should == false
242 | end
243 |
244 | it "should be an Integer given ''" do
245 | search = User.search
246 | search.id_gt = ''
247 | search.id_gt.should == 0
248 | end
249 |
250 | it "should be an Integer given 1" do
251 | search = User.search
252 | search.id_gt = 1
253 | search.id_gt.should == 1
254 | end
255 |
256 | it "should be an Integer given '1'" do
257 | search = User.search
258 | search.id_gt = "1"
259 | search.id_gt.should == 1
260 | end
261 |
262 | it "should be a Float given 1.0" do
263 | search = Order.search
264 | search.total_gt = 1.0
265 | search.total_gt.should == 1.0
266 | end
267 |
268 | it "should be a Float given '1'" do
269 | search = Order.search
270 | search.total_gt = "1"
271 | search.total_gt.should == 1.0
272 | end
273 |
274 | it "should be a Float given '1.5'" do
275 | search = Order.search
276 | search.total_gt = "1.5"
277 | search.total_gt.should == 1.5
278 | end
279 |
280 | it "should be a Range given 1..3" do
281 | search = Order.search
282 | search.total_eq = (1..3)
283 | search.total_eq.should == (1..3)
284 | end
285 |
286 | it "should be a Date given 'Jan 1, 2009'" do
287 | search = Order.search
288 | search.shipped_on_after = "Jan 1, 2009"
289 | search.shipped_on_after.should == Date.parse("Jan 1, 2009")
290 | end
291 |
292 | it "should be a Time given 'Jan 1, 2009'" do
293 | search = Order.search
294 | search.created_at_after = "Jan 1, 2009"
295 | search.created_at_after.should == Time.zone.parse("Jan 1, 2009")
296 | end
297 |
298 | it "should be a Time given 'Jan 1, 2009 9:33AM'" do
299 | search = Order.search
300 | search.created_at_after = "Jan 1, 2009 9:33AM"
301 | search.created_at_after.should == Time.zone.parse("Jan 1, 2009 9:33AM")
302 | end
303 |
304 | it "should still convert for strings, even if the conversion is skipped for the attribute" do
305 | search = User.search
306 | search.whatever_at_after = "Jan 1, 2009 9:33AM"
307 | search.whatever_at_after.should == Time.zone.parse("Jan 1, 2009 9:33AM")
308 | end
309 |
310 | it "should convert the time to the current zone" do
311 | search = Order.search
312 | now = Time.now
313 | search.created_at_after = now
314 | search.created_at_after.should == now.in_time_zone
315 | end
316 |
317 | it "should skip time zone conversion for attributes skipped" do
318 | search = User.search
319 | now = Time.now
320 | search.whatever_at_after = now
321 | search.whatever_at_after.should == now.utc
322 | end
323 |
324 | it "should be an Array and cast it's values given ['1', '2', '3']" do
325 | search = Order.search
326 | search.id_equals_any = ["1", "2", "3"]
327 | search.id_equals_any.should == [1, 2, 3]
328 | end
329 |
330 | it "should type cast association conditions" do
331 | search = User.search
332 | search.orders_total_gt = "10"
333 | search.orders_total_gt.should == 10
334 | end
335 |
336 | it "should type cast deep association conditions" do
337 | search = Company.search
338 | search.users_orders_total_gt = "10"
339 | search.users_orders_total_gt.should == 10
340 | end
341 |
342 | it "should support Rails' date_select and datetime_select out of the box" do
343 | search = Company.search('created_at_after(1i)' => 2000, 'created_at_after(2i)' => 1, 'created_at_after(3i)' => 1)
344 | search.created_at_after.should_not be_nil
345 | search.created_at_after.should == Time.zone.local(2000, 1, 1)
346 | end
347 | end
348 | end
349 |
350 | context "#delete" do
351 | it "should delete the condition" do
352 | search = User.search(:username_like => "bjohnson")
353 | search.delete("username_like")
354 | search.username_like.should be_nil
355 | search.conditions["username_like"].should be_nil
356 | end
357 | end
358 |
359 | context "#ordering_by" do
360 | it "should return nil if we aren't ordering" do
361 | search = User.search
362 | search.ordering_by.should be_nil
363 | end
364 |
365 | it "should return the column name for ascending" do
366 | search = User.search(:order => "ascend_by_first_name")
367 | search.ordering_by.should == "first_name"
368 | end
369 |
370 | it "should return the column name for descending" do
371 | search = User.search(:order => "descend_by_first_name")
372 | search.ordering_by.should == "first_name"
373 | end
374 |
375 | it "should handle symbols" do
376 | search = User.search(:order => :descend_by_first_name)
377 | search.ordering_by.should == "first_name"
378 | end
379 | end
380 |
381 | context "#ordering_direction" do
382 | it "should return nil if we aren't ordering" do
383 | search = User.search
384 | search.ordering_direction.should be_nil
385 | end
386 |
387 | it "should return the column name for ascending" do
388 | search = User.search(:order => "ascend_by_ticket_request_event_occurs_at")
389 | search.ordering_direction.should == "ascend"
390 | end
391 |
392 | it "should return the column name for descending" do
393 | search = User.search(:order => "descend_by_ticket_request_event_occurs_at")
394 | search.ordering_direction.should == "descend"
395 | end
396 | end
397 |
398 | context "#method_missing" do
399 | context "setting" do
400 | it "should call named scopes for conditions" do
401 | User.search(:age_less_than => 5).proxy_options.should == User.age_less_than(5).proxy_options
402 | end
403 |
404 | it "should alias exact column names to use equals" do
405 | User.search(:username => "joe").proxy_options.should == User.username_equals("joe").proxy_options
406 | end
407 |
408 | it "should recognize conditions with a value of true where the named scope has an arity of 0" do
409 | User.search(:username_nil => true).proxy_options.should == User.username_nil.proxy_options
410 | end
411 |
412 | it "should ignore conditions with a value of false where the named scope has an arity of 0" do
413 | User.search(:username_nil => false).proxy_options.should == {}
414 | end
415 |
416 | it "should not ignore conditions with a value of false where the named scope does not have an arity of 0" do
417 | User.search(:username_is => false).proxy_options.should == User.username_is(false).proxy_options
418 | end
419 |
420 | it "should recognize the order condition" do
421 | User.search(:order => "ascend_by_username").proxy_options.should == User.ascend_by_username.proxy_options
422 | end
423 |
424 | it "should pass array values as multiple arguments with arity -1" do
425 | User.named_scope(:multiple_args, lambda { |*args|
426 | raise "This should not be an array, it should be 1" if args.first.is_a?(Array)
427 | {:conditions => ["id IN (?)", args]}
428 | })
429 | User.search(:multiple_args => [1,2]).proxy_options.should == User.multiple_args(1,2).proxy_options
430 | end
431 |
432 | it "should pass array as a single value with arity >= 0" do
433 | User.named_scope(:multiple_args, lambda { |args|
434 | raise "This should be an array" if !args.is_a?(Array)
435 | {:conditions => ["id IN (?)", args]}
436 | })
437 | User.search(:multiple_args => [1,2]).proxy_options.should == User.multiple_args([1,2]).proxy_options
438 | end
439 |
440 | it "should not split out dates or times (big fix)" do
441 | s = User.search
442 | s.created_at_after = Time.now
443 | lambda { s.count }.should_not raise_error
444 | end
445 |
446 | it "should not include blank values" do
447 | s = User.search
448 | s.conditions = {"id_equals" => ""}
449 | s.proxy_options.should == {}
450 | end
451 | end
452 | end
453 |
454 | context "#respond_to?" do
455 | it "should respond to created_at_lte" do
456 | s = User.search
457 | s.respond_to?(:created_at_lte).should == true
458 | end
459 |
460 | it "should respond to created_at" do
461 | s = User.search
462 | s.respond_to?(:created_at).should == true
463 | end
464 |
465 | it "should not respond to created_at_or_whatever" do
466 | s = User.search
467 | s.respond_to?(:created_at_or_whatever)
468 | end
469 | end
470 |
471 | context "delegation" do
472 | it "should return all when not given any conditions" do
473 | 3.times { User.create }
474 | User.search.all.length.should == 3
475 | end
476 |
477 | it "should implement the current scope based on an association" do
478 | User.create
479 | company = Company.create
480 | user = company.users.create
481 | company.users.search.all.should == [user]
482 | end
483 |
484 | it "should implement the current scope based on a named scope" do
485 | User.named_scope(:four_year_olds, :conditions => {:age => 4})
486 | (3..5).each { |age| User.create(:age => age) }
487 | User.four_year_olds.search.all.should == User.find_all_by_age(4)
488 | end
489 |
490 | it "should respond to count" do
491 | User.create(:username => "bjohnson")
492 | search1 = User.search(:username => "bjohnson")
493 | search2 = User.search(:username => "nosnhojb")
494 | search1.count.should == 1
495 | search2.count.should == 0
496 | end
497 |
498 | it "should respond to empty?" do
499 | User.create(:username => "bjohnson")
500 | search1 = User.search(:username => "bjohnson")
501 | search2 = User.search(:username => "nosnhojb")
502 | search1.empty?.should == false
503 | search2.empty?.should == true
504 | end
505 |
506 | it "should delegate to named scopes with arity > 1" do
507 | User.named_scope :paged, lambda {|start, limit| { :limit => limit, :offset => start }}
508 | User.create(:username => "bjohnson")
509 | search = User.search(:username => "bjohnson")
510 | search.paged(0, 1).count.should == 1
511 | search.paged(0, 0).count.should == 0
512 | end
513 | end
514 |
515 | context "yaml" do
516 | it "should load yaml" do
517 | pending
518 | time = Time.now
519 | search = User.search(:name_like => "Ben", :created_at_after => time)
520 | search.current_scope = {:conditions => "1=1"}
521 | yaml = search.to_yaml
522 | loaded_search = YAML.load(yaml)
523 | loaded_search.current_scope.should == {:conditions => "1=1"}
524 | loaded_search.name_like.should == "Ben"
525 | loaded_search.created_at_after.should == time
526 | end
527 | end
528 | end
529 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | Bundler.setup
2 | require 'searchlogic'
3 | require "pry"
4 | require "timecop"
5 |
6 | ENV['TZ'] = 'UTC'
7 | Time.zone = 'Eastern Time (US & Canada)'
8 |
9 | ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
10 | ActiveRecord::Base.configurations = true
11 |
12 | ActiveRecord::Schema.verbose = false
13 | ActiveRecord::Schema.define(:version => 1) do
14 | create_table :audits do |t|
15 | t.string :auditable_type
16 | t.integer :auditable_id
17 | end
18 |
19 | create_table :companies do |t|
20 | t.datetime :created_at
21 | t.datetime :updated_at
22 | t.string :name
23 | t.string :description
24 | t.integer :users_count, :default => 0
25 | end
26 |
27 | create_table :user_groups do |t|
28 | t.string :name
29 | end
30 |
31 | create_table :user_groups_users, :id => false do |t|
32 | t.integer :user_group_id, :null => false
33 | t.integer :user_id, :null => false
34 | end
35 |
36 | create_table :users do |t|
37 | t.datetime :created_at
38 | t.datetime :updated_at
39 | t.integer :company_id
40 | t.string :username
41 | t.string :name
42 | t.integer :age
43 | t.boolean :male
44 | t.string :some_type_id
45 | t.datetime :whatever_at
46 | end
47 |
48 | create_table :carts do |t|
49 | t.datetime :created_at
50 | t.datetime :updated_at
51 | t.integer :user_id
52 | end
53 |
54 | create_table :orders do |t|
55 | t.datetime :created_at
56 | t.datetime :updated_at
57 | t.integer :user_id
58 | t.date :shipped_on
59 | t.float :taxes
60 | t.float :total
61 | end
62 |
63 | create_table :fees do |t|
64 | t.datetime :created_at
65 | t.datetime :updated_at
66 | t.string :owner_type
67 | t.integer :owner_id
68 | t.float :cost
69 | end
70 |
71 | create_table :line_items do |t|
72 | t.datetime :created_at
73 | t.datetime :updated_at
74 | t.integer :order_id
75 | t.float :price
76 | end
77 | end
78 |
79 |
80 | Spec::Runner.configure do |config|
81 | config.before(:each) do
82 | class ::Audit < ActiveRecord::Base
83 | belongs_to :auditable, :polymorphic => true
84 | end
85 |
86 | class ::Company < ActiveRecord::Base
87 | has_many :orders, :through => :users
88 | has_many :users, :dependent => :destroy
89 | end
90 |
91 | class ::Cart < ActiveRecord::Base
92 | belongs_to :user
93 | end
94 |
95 | class ::UserGroup < ActiveRecord::Base
96 | has_and_belongs_to_many :users
97 | end
98 |
99 | class ::User < ActiveRecord::Base
100 | belongs_to :company, :counter_cache => true
101 | has_many :carts, :dependent => :destroy
102 | has_many :orders, :dependent => :destroy
103 | has_many :orders_big, :class_name => 'Order', :conditions => 'total > 100'
104 | has_many :audits, :as => :auditable
105 | has_and_belongs_to_many :user_groups
106 |
107 | self.skip_time_zone_conversion_for_attributes = [:whatever_at]
108 | end
109 |
110 | class ::Order < ActiveRecord::Base
111 | belongs_to :user
112 | has_many :line_items, :dependent => :destroy
113 | end
114 |
115 | class ::Fee < ActiveRecord::Base
116 | belongs_to :owner, :polymorphic => true
117 | end
118 |
119 | class ::LineItem < ActiveRecord::Base
120 | belongs_to :order
121 | end
122 |
123 | ::Company.destroy_all
124 | ::User.destroy_all
125 | ::Order.destroy_all
126 | ::LineItem.destroy_all
127 | end
128 |
129 | config.after(:each) do
130 | class ::Object
131 | remove_const :Company rescue nil
132 | remove_const :User rescue nil
133 | remove_const :Order rescue nil
134 | remove_const :LineItem rescue nil
135 | end
136 | end
137 | end
138 |
--------------------------------------------------------------------------------