├── .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 | --------------------------------------------------------------------------------