I have an Entry
model which has many Tag
s. Tag
s are added to an entry by typing them into a textbox on my form, via a tag_names
virtual attribute. Before validation on the Entry
model, the tag_names string is converted into actual Tag
objects using find_or_create_by_name
. The Tag
model also has validation to make sure the tag name matches a regex, which is run via the association.
My Entry model looks like this:
class Entry < ActiveRecord::Base
has_many :entry_tags
has_many :tags, :through => :entry_tags
before_validation :update_tags
attr_writer :tag_names
private
def update_tags
if @tag_names
self.tags = @tag_names.split(",").uniq.map do |name|
Tag.find_or_create_by_name(name.strip)
end
end
end
end
When I create a new Entry
object and assign it tags, everything works correctly -- the tags are not saved if there is a validation error on one of the Tag
s, and an error message is passed back. However, if I try to update an existing Entry object with an invalid tag, instead of passing back a message, my self.tags=
call (in update_tags
above) is throwing an exception with the validation error message. Even if I overwrite find_or_create_by_name
to actually just return a new object instead of calling create
, I get the same outcome.
It seems to me (and the docs seem to corroborate) that the tags=
call is actually saving my Tag
objects before the main record gets saved when the Entry
object already exists. Is there anything I can do to make this save not happen, or to stop it from raising an exception and just causing my save to return false?
I would try something like this:
class Entry < ActiveRecord::Base
has_many :entry_tags
has_many :tags, :through => :entry_tags
before_validation :update_tags
attr_writer :tag_names
validates_associated :tags
private
def update_tags
return unless @tag_names
current_tag_names = tags.map(&:name)
user_tag_names = @tag_names.split(",").uniq
#add in new tags
user_tag_names.each do |name|
next if current_tag_names.include?(name)
tags.build :name => name
end
#remove dropped tags
( current_tag_names - user_tag_names ).each do |name|
removed_tag = tags.find_by_name(name)
tags.delete(removed_tag)
end
end
end
This way you're only initializing the related models in your update_tags
action and so won't throw validation errors. I also added in the validates_associated :tags
so that errors on these related models can be reported back via the standard input form using error_messages_for :entry
.
Update included code for removing dropped tags.