has one of many
i just wrote a cool module, should make it into a plugin, if i had time.
has_one_of_many lets you make a has_many association look like a has_one at runtime. In my app, X has_many Y but each user can only make one Y per X. I wanted to push the association find down into the model but normally models dont know about current_user. There's other uses as well, could be quite handy any time an action is attending to a single, currently selected item in a collection.
So, for example, if Article has many reviews, but each user can write only one review, now you can say @article.review and get the current user's one.
In the Article model you'd declare
has_one_of_many :reviews, :by => :user_id
Then in your controller, you could say Article.current_review_user_id = current _user.id
Internally it does a self.reviews.find_by_user_id
Possible extensions:
- has_some_of_many to return a filtered collection. The problem is i dont like the idea of replacing, for example, #reviews with the filtered one, but could use alias_method_chain. Anyway, named finders can do this already.
- :condition => or other find options to better control the find, such as the the most recent
Note, my app also uses a home-grown version of nested_params http://rails.lighthouseapp.com/projects/8994/tickets/1202-add-attributes-writer-method-for-an-association , so I generate a review_attributes=() method too.
Some have commented this violates MVC, but I strongly disagree. First, filtering on user is just an example. The model never knows about current_user, it only knows there's an attribute value used to constrain the collection to a single item.
Here's the code:
# ------------------------------------
# has_one_of_many
# ------------------------------------
# eg has_one_of_many :reviews, :by => :user_id
# implements class methods:
# current_review_user_id=( value ) # sets the value used to query the has_many
# current_review_user_id # get the value used
# instance methods:
# review # => the current child, as if it were has_one
# review_attributes=( attribs ) # sets attributes in the current, or new if none
@@has_current = {} # :assoc => :attrib
@@current_value = {} # :assoc => value
# names of associations that has current
def self.current_ones
@@has_current.keys
end
#
def self.has_one_of_many(association_id, options={}) # publicnames = association_id.to_s
name = name.singularize
by = options[:by]
@@has_current[name] = by
self.class_eval %{ def self.current_#{name}_#{by}=(value) @@current_value['#{name}'] = valueend
def self.current_#{name}_#{by} @@current_value['#{name}']end
# instance methods
def #{name} #{namepl}.find_by_#{by}( @@current_value['#{name}'] )end
def #{name}_attributes=(new_attributes) record = self.#{names}.find_by_#{by}( @@current_value['#{name}'] )if record
record.update_attributes(new_attributes)
else
self.#{names}.new(new_attributes)end
end
}, __FILE__, __LINE__
end
# kludgey but handles :include => :review
# since the association now looks like a has_one,
# but since its not really an association, AR barfs
# so just delete it from the :include
class << self
def find_with_current_ones_removed(*args, &block)
current_ones.each do |name|
args.last[:include].delete(name.to_sym)
end if args.last[:include]
find_without_current_ones_removed( *args, &block )
end
alias_method_chain :find, :current_ones_removed
end
Comments
New Comment