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.
Related
I have (in Rails 3.2.13):
class User < ActiveRecord::Base
has_many :app_event_login_logouts
end
class AppEventLoginLogout < ActiveRecord::Base
belongs_to :user
end
and would like to get back something like:
AppEventLoginLogoug.select("id, type, users.email").joins(:user)
basically id, type from app_event_login_logouts and email from users but this doesn't seem to be working. What would be the correct syntax?
Try out following code:
ret = User.joins(:app_event_login_logouts).select('app_event_login_logouts.id, app_event_login_logouts.type, users.email')
ret.first.id # will return app_event_login_logouts.id
ret.first.email # will return users.email
...
AppEventLoginLogoug.find(:all,
{:include => [:users],
:select => ['id', 'type', 'users.email']})
I also checked the apidoc and found something like that:
result= AppEventLoginLogoug.find(:all,
:conditions => ['condition_here'],
:joins => [:users],
:select => 'whatever_to_select'
:order => 'your.order')
I have a very simple model like this:
class User < ActiveRecord::Base
has_many :cookies
has_many :fortunes, :through => :cookies
end
class Cookie < ActiveRecord::Base
belongs_to :user
belongs_to :fortune
end
class Fortune < ActiveRecord::Base
has_many :cookies
has_many :users, :through => :cookies
end
For a given user, u, I can do
u.fortunes
This will give me all the fortunes associated with this user via Cookies table. What I want to do is get all Fortunes not returned by u.fortunes.
I tried
Fortune.all(:limit => 5, :conditions => {:user => {:id._ne => u.id} })
but that doesn't work :(. I am new to ActiveRecord.
Thanks
try this:
Fortune.limit(5).where("id not in (?)", u.fortunes.map(&:id))
(I tried it on my own tables)
Or try this
Fortune.includes(:cookies).limit(5).where([ 'cookies.user_id != ? OR cookies.user_id IS NULL', u.id ])
Or with the syntax You use
Fortune.all(:include => :cookies, :limit => 5, :conditions => [ 'cookies.user_id != ? OR cookies.user_id IS NULL', u.id ])
The reason to not use include :users is to avoid one extra join.
EDIT:
The other suggestions are shorter, and I think also a little bit quicker when finding (no joins), I only wanted to show how to use associations.
You can do
ids_to_reject = u.fortunes.map(&:id)
Fortune.all(:limit => 5, :conditions => ["id not in (?)", ids_to_reject])
try this
#fortune=Fortune.find(:all).delete_if{|fortune| !fortune.user.nil? }
It will delete the fortunes which are belongs to user, and give us the remaining.
I'm building a recommendation method for users in my project. Users generate interest records whenever they view, create, comment or interact with objects (weighted depending on the action).
I've written a find method that looks at a user's interests, and then finds users who are also interested in those items. However, it is horrendously inefficient, making as many db calls as the user has interests (up to 50).
Here's a chopped down version of what's going on:
#User.rb
...
has_many :interests, :as => :interestable, :dependent => :destroy
def recommendations
recommendations = []
Interest.for(self).limit(50).each do |item|
recommendations << Interest.other_fans_of(item)
end
user_ids = recommendations.flatten.map(&:user_id).uniq
end
...
#interest.rb
...
belongs_to :user
belongs_to :interestable, :polymorphic => true
named_scope :for, lambda { |user| { :conditions => { :user_id => user.id } } }
named_scope :limit, lambda { |num| { :limit => num } }
named_scope :other_fans_of, lambda { |interest| { :conditions => { :interestable_type => interest.interestable_type, :interestable_id => interest.interestable_id } } }
default_scope :order => "weight DESC"
...
Are there any sql geniuses out there who can turn that into one nice clean db call?
Something like this should do the job. There might be prettier ways…
class User < ActiveRecord::Base
#...
def recommendations
# get a list of the relevant interests
the_interests = Interest.for(self).limit(50).map{|x| [x.interestable_type, x.interestable_id]}
# make some sql
conditions = the_interests.map{|x| "(`interestable_type`=? AND `interestable_id`=?)"}.join(" OR ")
# use ruby magic to make a valid finder and get the other user_ids
user_ids = Interest.all(:select => '`user_id`', :conditions => [conditions, *(the_interests.flatten)]).map(&:user_id).uniq
end
#...
end
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.
I've created a method that allows me to return all of the Books. I'd like to limit the books returned to those that are not not loaned. What do I need to add to available_books to ensure only unloaned books are returned. Can I leverage my preexisting loaned? method?
class Book < ActiveRecord::Base
has_many :book_loans
has_many :borrowers, :through => :book_loans, :source => :person
def loaned?
book_loans.exists?(:return_date => nil)
end
def self.available_books
#books = find(:all, :order => "title")
end
end
You can modify your find to make it look like this:
find(:all, :select => "books.*", :joins => :book_loans, :conditions => ['book_loans.return_date is null'], :order => "title")
First off you might want to consider using named scopes in place defining methods, so for example the available_books method you have written could be rewritten as
named_scope :available_books, :order => "title"
Which would allow you to write Book.available_books in the same way you are doing, but in addition you can chain multiple named scopes like Book.available_books.by_author("bob") (assuming you defined another named scope called by_author which took a name as a param.
For checking if it is loaned you could try something like:
named_scope :loaned, :joins => :book_loans, :conditions => { :book_loans => { :return_date => nil } }
Alternatively you should be able to use a string for the conditions in the same way that Vincent has done.