assigning collection values and exceptions

hi everyone,

I've this code in my Report model:

  has_many :report_reasons, :validate => true   has_many :reasons, :through => :report_reasons #, :uniq => true # :accessible => true

  def reason_attributes=(reason_attributes)     reasons.clear       reason_attributes.uniq.each do |reason|         reasons << Reason.find_or_create_by_content(reason)       end   end

What I'm trying to accomplish (and what works) is to have a form which can submit to a Report some Reason via fields_for (Reason :content field). So I just swipe existing associations with .clear and proceed to rebuild new ones. I like to keep things that way so I have my validation errors back, this is true especially when saving a new Report.

What happens here is that, according to rails' code, when the owner object, in this case Report, doesn't exists yet, I can assign to it every kind of Reason I can think of, most importantly even those that doesn't pass validation. Because validations happens at .save I've my errors and I'm quite happy.

However this is different when using update_attributes, for updating. The owner object (Report) already exists, and every invalid Reason assigned through << will simply result in a raised ActiveRecord::InvalidRecord exception. This is best described directly into rails' code in association_collection.rb (<<) and has_many_through_association.rb (insert_record).

Now I may trap this exception in controller and do something with it, however I lose in some way my validation error messages on my form, which is dynamic (add/remove reasons via js).

The reason why I'm not using :accessible or some other plugin to ease multimodel forms is that they wants to create a record in Reason, which is good, but they don't try to reuse it if it already exists. Instead they, of course, will duplicate rows, defeating my purpose.

I can post some other code if anybody have some suggestions on how to proceed.

Thanks.

FWIW and for posterity's sake I workarounded with the not-yet-dry code:

  def reason_attributes=(reason_attributes)     reasons.clear     reason_attributes.uniq.each do |attributes|       reason = Reason.find_or_initialize_by_content(attributes)       if self.new_record?         # Entering create.         reasons << reason # no matter if it's invalid       else         # Entering update.         if reason.valid?           if Reason.exists?(reason)             # Reason is valid and already exists, instead of duplicating             # a record just assing it.             reasons << reason           else             # Reason is valid but doesn't exists, build it.             reasons.build(attributes)           end         else           reasons.build(attributes) # workarounded to show error messages         end       end     end   end

i could be wrong but that seems functionally equivalent to http://pastie.org/263949, right?

RSL

hi Russel, no, they are different. I must differentiate the fact that a owner object, self (which is a Report), is a new record.

Because if it's the case, then rails permits to push objects to a collection even if they are not valid (via validates_associated :reasons), and triggering validation only on save, called from the create action of the controller.

if self already exists, and therefore self.new_record? returns false, every assignment made through the use of push (<<) will bombs and return instantly an ActiveRecord::InvalidRecord.

As I said this behaviour is best described directly into rails' code in association_collection.rb (<< method) and has_many_through_association.rb (insert_record method).

So, to avoid the need to rescue those errors in controller, probably losing my validation messages and due to the fact that this is an ajax form with a dynamic number of fields on update I must check a couple of things.

If the reason that I'm trying to pass passes validations (with .valid?) then I must check if it already exists in database, since I don't want duplicates in Reason table, and if yes assign it to the collection of self, which will not raise any exception since if it's in database it's already valid, if not present just do a build, which will not trigger any exception but kist making update_attributes in controller do its job.

if the reason is not valid, always on update, then I fake out a build in any case, since the record will not pass, but at least I have now my error messages right near the fields on a multimodel form, reusing data from the collection.

you could still use your new_record? checks etc without using reasons.build to recreate the same object you already have from the find_or_initialize. that was my point.

RSL

also, that in every single case you show here... you add the reason to reasons. there's no case [unless i'm missing it] that you do something besides add the new reason to the reasons collection.

RSL

you're right, as I wrote this code wasn't DRYed up yet, I wrote it in
a hurry.

besides, thanks to your advice I've shortened this up to

   def reason_attributes=(reason_attributes)      reasons.clear      reason_attributes.uniq.each do |attributes|        reason = Reason.find_or_initialize_by_content(attributes)        if self.new_record?          # Entering create.          reasons << reason # no matter if it's invalid        else          # Entering update.          if reason.valid?            reasons << reason          else            reasons.build(attributes) # workarounded to show error
messages          end        end      end    end

do you have further optimisation in mind or I'm missing something
obvious?

another attempt

def reason_attributes=(reason_attributes)    reasons.clear    reason_attributes.uniq.each do |attributes|      reason = Reason.find_or_initialize_by_content(attributes)      reasons << reason rescue reasons.build(attributes)    end end

    reason = Reason.find_or_initialize_by_content(attributes)     reasons << reason rescue reasons.build(attributes)

doesn't make sense to me. the first line already finds or creates a reason. if it raises an exception adding it to the reasons collection, i don't see why building a new one with the exact same information would solve anything. you've tried this?

RSL

I'll add some view code. It's a kind of workaround to keep reasons collection with something
filled in. the new and edit view shares a partial which is from a collection:

# _report.erb <div class="reason">   <dl class="form">    <% fields_for("report[reason_attributes]", reason) do |r| %>     <dt class="required">Reason <%= r.error_message_on :content %></dt>      <dd>       <%= r.text_field :content, { :index => nil, :autocomplete =>
'off' } %>      <%= link_to_function "remove", "$(this).up('.reason').remove()" %>     </dd>    <% end %>   </dl> </div>

# edit.erb <% form_for [:administration, @document, @report] do |f| -%> ... <div id="reasons">   <%= render :partial => 'reason', :collection => @report.reasons %> </div> <p><%= add_reason_link "Add a reason" %></p> <%= f.submit %> <% end -%>

# controller update action uses only if
@report.update_attributes(params[:report])

when you first access the edit page, _report is cycled through every
reason it finds, and so it's ok. the moment I edit a reason field, putting in there an invalid
attribute that doesn't pass the validation I need to rescue the
exception thrown by << somewhere. I decided to rescue this exception with a reasons.build(attributes),
which will instantiate a new reason on the invalid field, thus showing
the error message.