views:

704

answers:

4

Let's say I have the following code:

@sites = Site.find(session[:sites]) # will be an array of Site ids
@languages = Language.for_sites(@sites)

for_sites is a named_scope in the Language model that returns the languages associated with those sites, and languages are associated with sites using has_many through. The goal is for @languages to have a distinct array of the languages associated with the sites.

Instead of calling the Language object on the second line, I'd ideally like to say

@sites.languages

and have the same list returned to me. Is there any way to do that cleanly in Rails 2.1 (or edge)? I know associations and named scopes can extend the array object to have attributes, but unless I'm missing something that doesn't apply here. Any plugins that do this would be welcome, it doesn't have to be in core.

A: 

Your instance variable @sites is an Array object and not Site so I don't think named_scope can be used. You can open up Array class to achieve this effect though (yikes)

class Array

  def languages
    ...
  end

end
JasonOng
Right, I know Rails does that in an extendable fashion for certain other objects (such as Arrays returned from has_many associations). I was hoping there would be something a bit cleaner. Thanks for your answer though!
Rufo Sanchez
A: 

If you added a has_many or has_and_belongs_to_many linking languages to sites, then you could use an include and do something like this:

Site.find( :all, :conditions =>{:id => session[:sites]}, :include => :languages )

You can make a named scope to do the :id => session[:sites] thing, eg:

class Site
  named_scope :for_ids, lambda{ |x| {:conditions => {:id => x }
end

and then do

Site.for_ids(session[:sites]).find(:all, :include => :languages)

Hope this gives you some ideas

Orion Edwards
I know I can :include => languages, but that's not quite what I'm going for - I'm basically looking for a distinct list of languages given a collection of sites. And the find([array of ids]) already returns the sites, so I don't think that's much of a benefit for me... thanks for your answer though!
Rufo Sanchez
+2  A: 

Why not use named_scopes for both?

class Site
  named_scope :sites, lambda{|ids| :conditions => "id in (#{ids.join(',')})"}
  named_scope :languages, :include => :languages ... (whatever your named scope does)
end

call:

Site.sites(session[:sites]).languages

or, if you want language objects back

Site.sites(session[:sites]).languages.collect{|site| site.languages}.flatten

You can also do it directly on the Language object. I'm using :joins because Rails 2.1 splits up and :include into two queries which means we can't use sites in the :conditions

class Language
  named_scope :for_sites, lambda{|site_ids| :joins => 'inner join sites on languages.site_id = sites.id' :conditions => "sites.id in (#{site_ids.join(',')})"}
end

call:

Language.for_sites(session[:sites])

In both examples I've assumed that session[:sites] is completely controlled by you and not subject to SQL injection. If not, make sure you deal with cleaning up the ID's

Andrew
Your second try is actually pretty close (just needs a .uniq call), but I was sort of hoping I could get some syntactic sugar for that nice Ruby Fresh feeling. Your Language ideas is basically exactly what I have right now, although slightly different as I have a site_language model. Thanks though!
Rufo Sanchez
+2  A: 

You could extend the array returned by Site.find.

class Site
  def find(*args)
    result = super
    result.extend LanguageAggregator if Array === result
    result
  end
end

module LanguageAggregator
  def languages
    Language.find(:all, :conditions => [ 'id in (?)', self.collect { |site| site.id } ])
  end
end
Matt Burke
Ah, so that's how the other methods do it... Thanks, this is definitely the closest, and even looks like it could be extracted out to be a module and/or plugin.
Rufo Sanchez