Rails find won't join when args appended - ruby-on-rails

I'm hoping I'm doing something wrong here and someone can point me in the right direction...
I have written a method inside of one of my models, here is the code:
def self.find_by_user_id(user_id, *args)
self.find(:all, :joins => :case_map,
:conditions => ['case_maps.uer_id = ? and case_maps.case_id = cases.id', user_id],
*args)
end
I can call the code like this and it works as expected:
Case.find_by_user_id(some_user_id)
However, when this code is executed with any additional args, like this:
Case.find_by_user_id(some_user_id, :limit => 15)
I get back ALL cases. The query in my log file show that it executed this:
Case Load (0.6ms) SELECT * FROM `cases` LIMIT 15
I even put a logger.info message in that method to make sure it's the one that is executing... It seems that whenever *args is not nil, that it skips all of the conditions and joins I added to the find and just uses the *args.
Anyone see something that I'm doing wrong?

AR::B#find expects a variable number of arguments, but the last of those should be the options hash. It uses Array#extract_options!, and so can you:
def self.find_by_user_id(user_id, *args)
options = args.extract_options!
options.merge!(:joins => :case_map, :conditions =>
['case_maps.uer_id = ? and case_maps.case_id = cases.id', user_id])
self.find(:all, *args, options)
end
But you shouldn't. What possible values make sense in between the :all and the options fields?
What you are really after is:
def self.find_by_user_id(user_id, options)
self.find(:all, options.merge(:joins => :case_map, :conditions =>
['case_maps.user_id = ? and case_maps.case_id = cases.id', user_id]))
end
Or one better:
named_scope :find_by_user_id, lambda{|user_id|
{
:joins => :case_map,
:conditions => ['case_maps.user_id = ? and case_maps.case_id = cases.id', user_id]
}
}
The named scope will ensure all your options are cleanly merged (all your conditions are applied even if you add more later on). You can then call it with:
Case.find_by_user_id(user_id).all(:limit => 15)
(Named Scope are “Da Bom”. One should use them as much as possible. Even in general conversation. For example, if you were here in my house last night, you may have overheard this little ditty: “What do you want for your dinner?”, “Same thing I have every night, Named Scopes and chips.”. Because I'm committed and all that.)
Also, as a side note, assuming this is on your Case model, the "and case_maps.case_id = cases.id" clause is unnecessary, :joins => :case_map does that for you.

Related

How to refactor complex method in Rails model with Rspec?

I have the following complex method. I'm trying to find and implement possible improvements. Right now I moved last if statement to Access class.
def add_access(access)
if access.instance_of?(Access)
up = UserAccess.find(:first, :conditions => ['user_id = ? AND access_id = ?', self.id, access.id])
if !up && company
users = company.users.map{|u| u.id unless u.blank?}.compact
num_p = UserAccess.count(:conditions => ['user_id IN (?) AND access_id = ?', users, access.id])
if num_p < access.limit
UserAccess.create(:user => self, :access => access)
else
return "You have exceeded the maximum number of alotted permissions"
end
end
end
end
I would like to add also specs before refactoring. I added first one. How should looks like others?
describe "#add_permission" do
before do
#permission = create(:permission)
#user = create(:user)
end
it "allow create UserPermission" do
expect {
#user.add_permission(#permission)
}.to change {
UserPermission.count
}.by(1)
end
end
Here is how I would do it.
Make the check on the Access more like an initial assertion, and raise an error if that happens.
Make a new method to check for an existing user access - that seems reusable, and more readable.
Then, the company limit is more like a validation to me, move this to the UserAccess class as a custom validation.
class User
has_many :accesses, :class_name=>'UserAccess'
def add_access(access)
raise "Can only add a Access: #{access.inspect}" unless access.instance_of?(Access)
if has_access?(access)
logger.debug("User #{self.inspect} already has the access #{access}")
return false
end
accesses.create(:access => access)
end
def has_access?(access)
accesses.find(:first, :conditions => {:access_id=> access.id})
end
end
class UserAccess
validate :below_company_limit
def below_company_limit
return true unless company
company_user_ids = company.users.map{|u| u.id unless u.blank?}.compact
access_count = UserAccess.count(:conditions => ['user_id IN (?) AND access_id = ?', company_user_ids, access.id])
access_count < access.limit
end
end
Do you have unit and or integration tests for this class?
I would write some first before refactoring.
Assuming you have tests, the first goal might be shortening the length of this method.
Here are some improvements to make:
Move the UserAccess.find call to the UserAccess model and make it a named scope.
Likewise, move the count method as well.
Retest after each change and keep extracting until it's clean. Everyone has a different opinion of clean, but you know it when you see it.
Other thought, not related to moving the code but still cleaner :
users = company.users.map{|u| u.id unless u.blank?}.compact
num_p = UserAccess.count(:conditions => ['user_id IN (?) AND access_id = ?', users, access.id])
Can become :
num_p = UserAccess.where(user_id: company.users, access_id: access.id).count

Ruby on Rails: Search on has_many association with inner join and other conditions

I've been searching for this for a long time now, and I can't get to find the answer anywhere.
What I have is a basic search function to find houses, everything works, and till so far the main piece for getting houses looks something like this (the conditions are for example):
#houses = House.all(
:conditions => ["houses.province_id = ? AND open_houses.date > ? AND houses.surface = ?", "4", "Tue, 08 Feb 2011", "125"],
:select => 'distinct houses.*',
:joins => "INNER JOIN open_houses ON open_houses.house_id = houses.id" )
Now, I have this has_many association for the specifications of a house (like balcony, swimming pool etc)..
For this I have the standard setup. A table for the spec-names, and a table with the house_id and the specification_id.
But now, I need to add these to the search function. So someone can find a house with Swimming pool AND a balcony.
I'm sure there is a solution, but I just don't know where to put it in the code, and how.. Google just get's me to pages like it, but not to pages explaining exactly this.
This is what my params look like:
Parameters: {"dateto"=>"", "commit"=>"ZOEKEN!", "pricefrom"=>"", "location"=>"", "province_id"=>"4", "rooms"=>"", "datefrom"=>"", "surface"=>"125", "utf8"=>"✓", "priceto"=>"", "filters"=>{"by_specifications"=>["2", "5", "10"]}, "house_category_id"=>""}
Hope someone can help, if something is unclear please let me know.
Thanks
EDIT:
Oke, I've got it to work! Thanks very much!
There's only one little problem: A house shows up if either one of the specs exists for the house.. So it's an OR-OR query, but what I want is AND-AND..
So if a user searches for balcony and garage, the house must only show up if both of these exist for the house..
What I did now is this: for each specification searched, a query is being made.. (code is below)
I'm wondering, is this the right way to go?
Because it works, but I get double matches.. ( I filter them using "uniq" )
The problem with uniq is that I can't get "will_paginate" to work with it..
This is my final code:
def index
ActiveRecord::Base.include_root_in_json = false
# I'm creating conditions with a function I made, PM me for the code..
conditions = createConditions( createParameters( ) );
query = House.includes( :open_houses ).where( conditions )
unless params[:specifications].blank?
query = query.joins( :house_specifications )
query = query.group( 'open_houses.id' )
query = query.where( 'house_specifications.specification_id' => params[:specifications] )
query = query.order( 'count(open_houses.id) DESC' )
# query = query.having( 'count(open_houses.id) = ?', [params[:specifications].length.to_s] )
end
query = query.order( (params[:sort].blank?)? "open_houses.date ASC, open_houses.from ASC" : params[:sort] )
if params[:view] == "list"
page = params[:page] unless params[:page].blank?
#houses = query.all.uniq.paginate( :page => page || "1", :per_page => 5 )
else
#houses = query.all.uniq
end
respond_to do |format|
format.html
format.js
end
end
Thanks for the help, really appreciate it!
Try this (Rails 3):
House.joins(:houses_specifications).
where('houses_specifications.specification_id' => params[:by_specifications]).
where(...).all
You can have a bunch of if's and case's to add filters, group_by's, limits etc before you run that final .all on the query.
house_query = House.where(:pink => true)
unless params[:by_specifications].blank?
house_query = house_query.joins(:houses_specifications).
where('houses_specifications.specification_id' => params[:by_specifications])
end
...
#houses = house_query.all
Edit:
New and improved version that doesn't query directly on the join table for readability, group_by for distinctness and does an intersect to get houses with all specs.
query = House.joins(:open_houses).where(conditions)
unless params[:specifications].blank?
query = query.joins(:specifications).
group('houses.id').
where(:specifications => params[:specifications]).
having('count(*)=?', params[:specifications].count)
end
if params[:view] == "list"
#houses = query.all.paginate(:page => params[:page])
else
#houses = query.all
end
You can change the "having" to an "order" to get those with the most matching specs first.

Rails doing a FIND with Conditions?

In Rails 3, I created a Search Form that does a FIND with conditions in the Models file.
#projects = find(:all,
:select => 'projects.*',
:conditions => ['name = ?', search_name]
).first
This works great if a name is provided in the searchform (search_name). Problem is if search_name is blank, Rails Errors (can't say I blame it)...
What is the smart way to handle this situation? I'd like, if search_name is blank, to not error but return everything.
Suggestions? Thanks!
You can create a scope to handle this. In your Project model, add something like:
scope :search_by(name), lambda{|name| first.where(:name => name) unless name.blank?}
then in your controller, simply call:
Project.search_by(params[:search])
EDIT:
If you need to serach for multiple fields you can adapt the scope:
scope :search_by(name), lambda{|name| first.includes(:owner).where("projects.name LIKE ? OR owners.name LIKE ?", name, name) unless name.blank?}
if search_name.blank?
#projects = Project.order(:name)
else
#projects = Project.where(:name => search_name)
end
The cleanest way is using lazy loading with the new ActiveRecord functionalities like this:
#projects = Project.order(:name)
#projects = #projects.where(:name => search_name) if search_name
You can add as many conditions as you like this way. They won't be executed until you need the results anyway (with #projects.all or #projects.each, etc...)

Is it possible to filter by conditions before paginating?

I'm using ruby on rails 2.3.8 and will_paginate plugin.
I've just noticed that if I write something like this:
Announcement.paginate :page => params[:page], :per_page => 10, :conditions => some_condition
it will work.
But, if I write something like this:
announcements = Announcement.all :conditions => some_condition
#ann = announcements.paginate :page => params[:page], :per_page => 10
it won't recognize conditions.
EDIT:
I've developed a Search functionality and, due to a Sort functionality I had to implement, I had to put the search feat inside a model's method to call it from the controller every time I need either to search or sort by some field.
So, my model's methods look like this:
def self.search_by_relevance(words)
conditions = get_search_conditions(words)
Announcement.published.descend_by_featured.order_by_rate :conditions => conditions
end
where "published" and "order_by_rate" are named scopes and "descend_by_feature" belongs to "searchlogic" gem.
def self.get_search_conditions(words)
unless words.empty? or words.nil?
conditions = ''
words.each do |word|
if conditions.nil? or conditions.empty?
conditions = '(title like "%' + word + '%" or description like "%' + word + '%")'
else
conditions += ' and (title like "%' + word + '%" or description like "%' + word + '%")'
end
end
conditions
end
end
My controller's action looks like this:
def search
#announcements = Announcement.search_by_relevance(params[:txtSearch].to_s.split).paginate :page => params[:page], :per_page => 10 unless params[:txtSearch].nil? or params[:txtSearch].empty?
end
This syntax won't recognize the conditions specified in the model's method.
EDIT 2:
Thanks for the posts. Testing my code a little more I found out that if I write ".all" right after "order_by_rate" at this line Announcement.published.descend_by_featured.order_by_rate :conditions => conditions, in search_by_relevance method it will return the correct query, but will_paginate plugin will give me the following error(just if I add ".all"):
NoMethodError in AnnouncementsController#search
undefined method `to_i' for {:page=>nil, :per_page=>10}:Hash
D:/Proyectos/Cursometro/www/vendor/plugins/will_paginate/lib/will_paginate/collection.rb:15:in `initialize'
D:/Proyectos/Cursometro/www/vendor/plugins/will_paginate/lib/will_paginate/core_ext.rb:37:in `new'
D:/Proyectos/Cursometro/www/vendor/plugins/will_paginate/lib/will_paginate/core_ext.rb:37:in `paginate'
D:/Proyectos/Cursometro/www/app/controllers/announcements_controller.rb:276:in `search'
First of all, I don't understand why I have to add the ".all" to the query to work right, and second, I don't see why will_paginate won't work when I include ".all"(I also tried to add the following code but didn't work: :page => params[:page] || 1).
Also, if I include the ".all" syntax to the query, it will return:
SELECT * FROM announcements WHERE
((title like "%anuncio%" or
description like "%anuncio%")) AND
(announcements.state = 'published')
ORDER BY announcements.featured DESC
If I don't, it will return:
SELECT * FROM announcements WHERE
(announcements.state = 'published')
ORDER BY announcements.featured DESC
Do you see that no conditions are being included in the last one? This is causing the problem.
I don't know if this will work for you, but you can use paginate just like find, I mean, something like:
#announcements = Announcement.paginate_all_by_id params[:id],
:page => params[:page], :per_page => 10
Edit
#announcements is an array, right?
Well, I found this post and this other one that may help you.
Well, I kind of solve this by adding ".paginate"(instead of ".all") to my query in the model's method, passing by parameters the "page" and "per_page" values. It was not my idea to include pagination in models, but well...it's the solution I have for now. If you come up with a better one, I'll be glad to hear it :)

will_paginate without use of activerecord

I apologize if this is a trivial question or my understanding of rails is weak.
I have 2 actions in my controller, index and refine_data.
index fetches and displays all the data from a database table.
refine_data weeds out unwanted data using regex and returns a subset of the data.
Controller looks like:
def index
Result.paginate :per_page => 5, :page => params[:page], :order => 'created_at DESC'
end
def refine_data
results = Result.all
new_results = get_subset(results)
redirect_to :action => 'index'
end
I would like to redirect the refine_data action to the same view (index) with new_results.
As new_results are not from the database table (or model), how do I go about constructing my paginate?
As I wrote in my answer to Sort by ranking algorithm using will-paginate, it is possible to use a custom find method to order the data.
It could be used similar to filter out unwanted data, since you just need to return a set of data. By modifying the name of your refine_data to something like find_refined_data you can use
Result.paginate_refined_data :per_page => 5, :page => params[:page], :order => 'created_at DESC'
in your index method. (Of course you need to return a set of records instead redirect to the index action)
BTW you could also use the paginate_by_sql method, if you are able specify your filter as a SQL query. This is probably more efficient than grabbing all records and performing a regex on them. But more complex I guess.
I was not successful in getting will_paginate to work by creating my own find method.
I was almost successful but not quite.
Here's what I've tried:
In Controller:
def refine_data
Result.paginate_refined_data :per_page => 5, :page => params[:page], :order => 'created_at DESC', :exclude =>"somestring"
end
In Model:
def find_refined_data(args)
exclude_string = args[:exclude];
new_results = do_some_work_and_exclude_records(#results,exclude_string)
end
will_paginate had trouble with me passing an additional parameter :exclude which it did not understand.
The simplest solution for me was to create my own WillPaginate::Collection object.
So here's how mine works now:
#The method name does not cause will_paginate to intercept and try to do its magic.
def new_find_refined_data(args)
exclude_string = args[:exclude];
new_results = do_some_work_and_exclude_records(#results,exclude_string)
#entries = WillPaginate::Collection.create(1, args[:per_page]) do |pager|
# inject the result array into the paginated collection:
pager.replace(new_results)
unless pager.total_entries
# the pager didn't manage to guess the total count, do it manually
pager.total_entries = new_results.length
end
end
end
Hope this will help any of the guys facing the same problem:
I'm not sure about the other answers - you may well want to move that method into your model class.
But answering your actual question.
The way to show the same view as another action is not to redirect but use:
render :action => :index
Yet another alternative might be to create a named_scope for your refined result set.
Can you formulate your "get_subset" business logic into database-style commands?
If so you can reconstruct your finder as a named_scope (with those conditions)
eg:
named_scope :blue_things, :conditions => {:colour => 'blue'}
and then just call paginate on that:
Result.blue_things.paginate :per_page => 5, :page => params[:page]

Resources