views:

114

answers:

1

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.)

A: 

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.

austinfromboston
I actually took a look at Paperclip; it lacks some of the more sophisticated features that I happen to need. (Assets are used nontrivially in my app in many places and sometimes polymorphically; having one psuedocolumn per asset type would be bad.)
Sai Emrys
Also - is there any way that would enable me to do something like: Post.find(1).comments(:include => {:users => {:avatar_asset => :thumb }})?
Sai Emrys
... also, AFAICT your code doesn't work. Even adding the necessary :class_name and :foreign_key, your avatar_assets barfs: `Unknown column 'users.asset_id'` 'cause it doesn't know what user it's on. But I think this is leading me to something better; will need to reread about association extensions.
Sai Emrys
Re. your edit - that's not what I meant. It's "include the avatars of users who commented on this post, so I can cache the whole damn thing". Not "find only the comments with avatar-having users".
Sai Emrys