named_scope dependent on existence of association is breaking tests - ruby-on-rails

User model:
class User < ActiveRecord::Base
named_scope :clients,
:conditions => "roles_users.role_id = #{Role.find_by_name('client').id}"
end
When testing, throws error:
Called id for nil, which would mistakenly be 4 -- if you really wanted (etc.)
Role fixtures:
client:
name: client
user:
name: user
Apparent problem: Rails is loading this class before it loads fixtures. When it loads the class it evaluates the named_scope. There are no roles at that point, so it blows up.
Possible solution:
named_scope :clients,
lambda { { :conditions => "roles_users.role_id = #{Role.named('client').id}" } }
However, I am not pleased with this solution, seeing as it introduces additional complexity and presumably a (small?) performance hit, just so that tests run properly. I'd like an alternative. Can you help?

The solution you propose is the correct solution. I would also recommend changing your code to:
named_scope :clients, lambda { { :conditions => ['roles_users.role_id = ?', Role.named('client').id } }
An alternative might be:
named_scope :clients, :joins => :role, :conditions => ['roles.name = ?', 'client']
You might also want to think about doing:
named_scope :with_role, lambda { |r| { :conditions => ['roles_users.role_id = ?', r.id] } }
Or even (for extra points)
Role.find_by_name('client').users
Anyway, I hope this helps.

Related

Does find(:all) on a has_many :through simply not work?

I have some model classes like this:
class Organisation < ActiveRecord::Base
has_many :dongles
has_many :licences_on_owned_dongles, :through => :dongles, :source => :licences,
:include => [:organisation, :user, :owner_organisation, :profile, :dongle,
{:nested_licences => [:profile]} ]
end
class Dongle < ActiveRecord::Base
has_many :licences
belongs_to :organisation
end
class Licence < ActiveRecord::Base
belongs_to :dongle
# tree-like structure. I don't remember why this had to be done but the comment says
# "find a way to make the simpler way work again" and I tried using the simpler way
# but tests still fail. So obviously the SQL awfulness is necessary...
default_scope :conditions => { :parent_licence_id, nil }
has_many :nested_licences, :class_name => 'Licence', :dependent => :destroy,
:autosave => true,
:foreign_key => :parent_licence_id,
:finder_sql => proc {
"SELECT l.* FROM licences l WHERE l.parent_licence_id = #{id}" },
:counter_sql => proc {
"SELECT COUNT(*) FROM licences l WHERE l.parent_licence_id = #{id}" }
end
Now I can do this:
test "getting licences on owned dongles" do
org = organisations(:some_other_corp)
assert_equal [licences(:licence_4)], org.licences_on_owned_dongles
end
That happily passes. Since it's an association, you might thing you can find() on it:
test "getting licences on owned dongles and then filtering further" do
org = organisations(:some_other_corp)
conditions = { :owner_organisation_id => nil }
assert_equal [licences(:licence_4)],
org.licences_on_owned_dongles.find(:all, :conditions => conditions)
end
But this gives:
ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: dongles.organisation_id: SELECT "licences".* FROM "licences" WHERE "licences"."parent_licence_id" IS NULL AND (("dongles".organisation_id = 72179513)) AND ("licences".parent_licence_id = 747059259)
test/unit/organisation_test.rb:123:in `test_getting_licences_on_owned_dongles_and_then_filtering_further'
In fact, this even occurs when all you call is find(:all). It isn't just SQLite either, because I noticed this in production (oops) on MySQL.
So I don't know. It's really too mysterious to investigate further. I might shelve it as a "Rails just can't do find() on an association", use a block to filter it and leave it at that. But I wanted to put it out, just in case there is a better option.
(Actually if you look at the query Rails is generating, it is complete nonsense. Somehow it has ended up generating a query where something has to be NULL and equal to a value at the same time. Even if the query worked, this will return 0 rows.)
Don't use find in a Rails 3 app.
org.licences_on_owned_dongles.find(:all, :conditions => conditions)
should be
org.licences_on_owned_dongles.where(conditions)
Edit: Read up on it here.
I think you're looking for .where:
org.licenses_on_owned_dongles.where(conditions)

Silblings in has_many relationship

A user has many employments.
What do you think?
Is this a valid and clear way to fetch all siblings (belonging to the same user) of a given employment object?
class Employment < ActiveRecord::Base
belongs_to :user
has_many :silblings,
:primary_key => :user_id,
:foreign_key => :user_id,
:class_name => 'Employment'
end
This can be extended with the following named scope:
named_scope :except, lambda {|id| {:conditions => ["id != ?", id]} if id}
Now I can do stuff like:
self.silblings.except(self.id).each do |silbling|
puts silbling
end
The resulting SQL statement looks like:
SELECT * FROM `employments`
WHERE (`employments`.user_id = 49)
AND ((id != 46) AND (`employments`.user_id = 49))
Comments like 'no, you abuse XY, rather use this XZ' are very welcome!
Reto
Looks fine. Except that the SQL doubles ('employments'.user_id = 49) in the query. Which is nothing major. If it's something you really don't want, you could go about defining siblings like this:
class Employment < ActiveRecord::Base
belongs_to :user
named_scope :for_user, lambda { |user|
{ :conditions => {:user_id => user} }
}
named_scope :except, lambda {|employment|
{:conditions => ["id != ?", employment}
}
def siblings
Employment.for_user(user_id).except(id)
end
end
Believe it or not you can still call named scopes on #employment.siblings. Although doing things this way means you can't assign to siblings. The siblings call comes out a little cleaner. There may be a performance improvement, but it probably won't be significant to make a difference.

Change a finder method w/ parameters to an association

How do I turn this into a has_one association?
(Possibly has_one + a named scope for size.)
class User < ActiveRecord::Base
has_many :assets, :foreign_key => 'creator_id'
def avatar_asset size = :thumb
# The LIKE is because it might be a .jpg, .png, or .gif.
# More efficient methods that can handle that are OK. ;)
self.assets.find :first, :conditions =>
["thumbnail = '#{size}' and filename LIKE ?", self.login + "_#{size}.%"]
end
end
EDIT: Cuing from AnalogHole on Freenode #rubyonrails, we can do this:
has_many :assets, :foreign_key => 'creator_id' do
def avatar size = :thumb
find :first, :conditions => ["thumbnail = ? and filename LIKE ?",
size.to_s, proxy_owner.login + "_#{size}.%"]
end
end
... which is fairly cool, and makes syntax a bit better at least.
However, this still doesn't behave as well as I would like. Particularly, it doesn't allow for further nice find chaining (such that it doesn't execute this find until it's gotten all its conditions).
More importantly, it doesn't allow for use in an :include. Ideally I want to do something like this:
PostsController
def show
post = Post.get_cache(params[:id]) {
Post.find(params[:id],
:include => {:comments => {:users => {:avatar_asset => :thumb}} }
...
end
... so that I can cache the assets together with the post. Or cache them at all, really - e.g. get_cache(user_id){User.find(user_id, :include => :avatar_assets)} would be a good first pass.
This doesn't actually work (self == User), but is correct in spirit:
has_many :avatar_assets, :foreign_key => 'creator_id',
:class_name => 'Asset', :conditions => ["filename LIKE ?", self.login + "_%"]
(Also posted on Refactor My Code.)
Since there are actually multiple avatar_assets ( one for each size ), you have to keep it as a has_many association.
class User < AR::B
has_many :avatar_assets, :conditions => ['filename like ?' '%avatar%'], :class_name => 'Asset'
named_scope :avatar_size, lambda { |size|
{ :conditions => [ "thumbnail = ?", size ] }
}
end
An alternative would be to put all the work in the named scope:
class User < AR::B
named_scope :avatar_for, lambda { |user, options|
if options[:size]
{ :conditions => [ "filename like ? AND thumbnail = ?", user.login, options[:size] ] }
else
{ :conditions => [ "filename like ?", user.login ] }
end
}
end
this allows you to say
Asset.avatar_for(current_user, :size => :medium)
but is less cool when you find yourself saying
current_user.avatar_for( current_user, :size => :medium )
you could add some :avatar, :avatar?, etc methods to User to clean this up.
Personally I advise you to check out the Paperclip plugin and avoid these issues entirely.
EDIT:
Per your comment, to create a condition like "show me comments by avatar-having users", I'm not sure that will do it. You'd could make a relationship like so:
class Comment
named_scope :with_avatars, :include => { :user => :avatar_assets }, :conditions => [ 'assets.thumbnail = ?', :thumb ]
end
EDIT:
Since you're only interested in caching, rather than conditions, we can drop the condition array:
named_scope :with_avatars, :include => { :user => :avatar_assets }
I revised the code above to be more workable. The key difference is to make the 'avatar'-ness of the assets easily queryable. If you can update your existing avatar_assets to have a filename including the pattern 'avatar-[login]', you can make the condition set static which is much cleaner than always having to search for the avatar based on the user login. Association extensions are another way to resolve this, however I don't think you'll be able to chain them or combine them with named scopes.

How do I re-use named scopes?

Hi I have a named_scope in my User model as following.
named_scope :by_gender, lamdba { |gender| { :conditions => { :gender => gender } } }
I want to create other two named scopes which re use this one something like,
named_scope :male, lambda { by_gender('male') }
named_scope :female, lambda { by_gender('female') }
Any idea what to do?
You could provide class methods that perform the hardwired argument passing:
def self.male
by_gender('male')
end
def self.female
by_gender('female')
end
or, as the named_scope you are using is so simple you could cut out the by_gender scope and simply use:
named_scope :male, :conditions => {:gender => 'male'}
named_scope :female, :conditions => {:gender => 'female'}
The second option is of course conditional on you not actually requiring the by_gender scope explicitly anywhere else.

Is there a way to combine named scopes into a new named scope?

I have
class Foo < ActiveRecord::Base
named_scope :a, lambda { |a| :conditions => { :a => a } }
named_scope :b, lambda { |b| :conditions => { :b => b } }
end
I'd like
class Foo < ActiveRecord::Base
named_scope :ab, lambda { |a,b| :conditions => { :a => a, :b => b } }
end
but I'd prefer to do it in a DRY fashion. I can get the same effect by using
Foo.a(something).b(something_else)
but it's not particularly lovely.
At least since 3.2 there is a clever solution :
scope :optional, ->() {where(option: true)}
scope :accepted, ->() {where(accepted: true)}
scope :optional_and_accepted, ->() { self.optional.merge(self.accepted) }
Well I'm still new to rails and I'm not sure exactly what you're going for here, but if you're just going for code reuse why not use a regular class method?
def self.ab(a, b)
a(a).b(b)
end
You could make that more flexible by taking *args instead of a and b, and then possibly make one or the other optional. If you're stuck on named_scope, can't you extend it to do much the same thing?
Let me know if I'm totally off base with what you're wanting to do.
By making it a class method you won't be able to chain it to an association proxy, like:
#category.products.ab(x, y)
An alternative is applying this patch to enable a :through option for named_scope:
named_scope :a, :conditions => {}
named_scope :b, :conditions => {}
named_scope :ab, :through => [:a, :b]
Yes Reusing named_scope to define another named_scope
I copy it here for your convenience:
You can use proxy_options to recycle one named_scope into another:
class Thing
#...
named_scope :billable_by, lambda{|user| {:conditions => {:billable_id => user.id } } }
named_scope :billable_by_tom, lambda{ self.billable_by(User.find_by_name('Tom').id).proxy_options }
#...
end
This way it can be chained with other named_scopes.
I use this in my code and it works perfectly.
I hope it helps.
#PJ: you know, I had considered that, but dismissed it because I thought I wouldn't be able to later chain on a third named scope, like so:
Foo.ab(x, y).c(z)
But since ab(x, y) returns whatever b(y) would return, I think the chain would work. Way to make me rethink the obvious!
Check out:
http://github.com/binarylogic/searchlogic
Impressive!
To be specific:
class Foo < ActiveRecord::Base
#named_scope :ab, lambda { |a,b| :conditions => { :a => a, :b => b } }
# alias_scope, returns a Scope defined procedurally
alias_scope :ab, lambda {
Foo.a.b
}
end

Resources