How to make named_scope aware of association context?

Given these models

  class Category < ActiveRecord
    has_many :category_assignments
    has_many :posts, :through => :category_assignments
  end

  class CategoryAssignment < ActiveRecord::Base
    belongs_to :post
    belongs_to :category
    # Has boolean column 'featured'
  end

  class Post < ActiveRecord::Base
    has_many :category_assignments
    has_many :categories, :through => :category_assignments
  end

I want to add a named_scope to Post that will return all featured
posts. But the semantics of this named_scope vary depending upon
whether I am calling the named scope on Post or on category.posts:
  Post.featured
is simple enough. The definition
  named_scope :featured,
              :joins => :category_assignments,
              :conditions => {:category_assignments.featured => true }
will return all posts for which any associated category_assignment
record has featured = true.

But when I take a category instance and call category.posts.featured I
want to get all of the posts which are featured *in that category*,
which requires restricting by category_id. To do this properly the
named_scope code needs to be able to determine whether it's being
called in a context in which category_assignments has already been
joined (and/or whether a category_id is specified in the conditions).
I have not figured out how to do this.

If I can detect whether or not the named_scope is being called within
the context of an association then I can do something like the
following, which changes named_scope semantics based on whether or not
a category _argument_ is explicitly provided:

  named_scope :featured, lambda { |*args|
    if (category = args.first)
      { :joins => :category_assignments,
        :conditions => {:category_id => category.id,
                        :featured => true} }
    else
      { :joins => :category_assignments,
        :conditions => {:featured => true} }
    end
  }

Is there some way to inspect a joins or conditions hash to determine
association state from within a named_scope? And if so, is it reliable
regardless of the order of named_scope stacking (i.e. will it work for
either category.posts.other.named.scopes.featured or
category.posts.featured.other.named.scopes)?

Thanks,

Sven

It should do that by itself. You shouldn't need to do anything. Have
you tried it?

Fred

Yes, with code like this:

named_scope :featured, :joins => :category_assignments,
:conditions => [‘category_assignments.featured = ?’, true ]

When I call

Post.featured

it works fine. But when I call, category.posts.featured I get

ActiveRecord::StatementInvalid: SQLite3::SQLException: ambiguous column name: category_assignments.post_id

because Rails generates this query:

SELECT “posts”.* FROM “posts”

INNER JOIN “category_assignments” ON category_assignments.post_id = post.id

INNER JOIN category_assignments ON posts.id = category_assignments.post_id

WHERE ((“category_assignments”.category_id = 1))

AND ((category_assignments.featured = ‘t’))

As you can see, category_assignments is being joined twice. The named_scope needs to omit the category_assignments join when it gets called on a Category instance.

I’m running this on Rails 2.1.1, by the way.

-Sven

PS: there was an error in my initial post. I specified
:conditions => {:category_assignments.featured => true }
where I ought to have written

:conditions => [‘category_assignments.featured = ?’, true]

It should do that by itself. You shouldn't need to do anything. Have
you tried it?

Fred

Yes, with code like this:

  named_scope :featured, :joins => :category_assignments,
                       :conditions =>
['category_assignments.featured = ?', true ]

When I call

  Post.featured

it works fine. But when I call, category.posts.featured I get

  ActiveRecord::StatementInvalid: SQLite3::SQLException: ambiguous
column name: category_assignments.post_id

because Rails generates this query:

  SELECT "posts".* FROM "posts"
  INNER JOIN "category_assignments" ON category_assignments.post_id
= post.id
  INNER JOIN category_assignments ON posts.id =
category_assignments.post_id
  WHERE (("category_assignments".category_id = 1))
  AND ((category_assignments.featured = 't'))

As you can see, category_assignments is being joined twice. The
named_scope needs to omit the category_assignments join when it gets
called on a Category instance.

Oops. I skimmed over the definition of your scope and didn't notice
that there was a join. I'm going out on a limb here but from inside a
procedural named scope then if there is a @proxy_owner instance
variable or if it responds to proxy_owner/proxy_target then it;s an
association. You could also check the current scope and see what
joins are in there.

Fred

That does seem promising, but I haven't gotten it to work yet. Both
proxy_owner and proxy_target remain undefined (on self) and
@proxy_owner remains null regardless of which way I invoke the
named_scope lambda. You also suggested checking the current scope for
joins but that's precisely the thing I don't know how to do, which
prompted my initial post. I'm still working on it, though.

-Sven