Rails: adding existing records with nested form and has_many :through

I just had a long session solving this problem and the internet could not really help. What I was trying to accomplish: An image upload, where a content can have multiple images, and multiple image types (profile, gallery images), and each Image can belong to different content types or models. The image gets uploaded with drag and drop and marked as temporary, when the parent model is saved, the association to the images should be created and the attributes of the image like title, author and alt should be updated.

I have to admit it is one of the more complicated setups, but a use case that it is rather common in my opinion. Forms always make trouble ;)

class ParentModel < ActiveRecord::Base
  has_many :gallery_image_assignments, ->{ order(:position) }, as: :imageable
  accepts_nested_attributes_for :gallery_image_assignments
  has_many :gallery_images, through: :gallery_image_assignments, source: :image
end
class Image < ActiveRecord::Base
  dragonfly_accessor :image

  has_many :image_assignments, dependent: :destroy
end
## STI

class GalleryImage < Image

end

class ImageAssignment < ActiveRecord::Base
  belongs_to :image
  belongs_to :imageable, polymorphic: true

  accepts_nested_attributes_for :image, update_only: true
end


class GalleryImageAssignment < ImageAssignment

end
## Parent Controller

def create
    @parent_model = ParentModel.new(parent_model_params)
...

def parent_model_params
      params.require(:parent_model).permit(gallery_image_ids: [], gallery_image_assignments_attributes: [:id, :position, :image_id, image_attributes:[:id, :title, :alt, :author]])
    end
##Form: _form.html.erb
<div class="image-upload" data-path="<%= gallery_images_path(format: :json) %>" data-association="gallery_image_assignments">
  <%= file_field_tag :gallery_image_upload, multiple: true %>
  <%= f.fields_for :gallery_image_assignments do |gi| %>
    <%= render 'images/image_field', f: gi, pf: f %>
  <% end %>

  <script type="text/template" class="image-field-tpl">
      <%= field_markup(f, :gallery_image_assignments, 'images/image_field', {pf: f}) %>
  </script>
  </div>

## Image Field partial
<div>
<%= f.text_field :position, placeholder: "Position" %>
<%= f.hidden_field :image_id, {class: "image-id-field"} %>
<%= f.fields_for :image do |ifield| %>
  <p><%= image_tag ifield.object.present? ? ifield.object.thumb_url : "" %></p>
  <p><%= ifield.text_field :title, placeholder: "Title" %></p>
  <p><%= ifield.text_field :alt, placeholder: "Alt" %></p>
  <p><%= ifield.text_field :author, placeholder: "Author" %></p>
<% end %>
</div>

How it works: One or multiple images are uploaded with ajax, on success a new form element gets appended and the image id and thumbnail url is set. The problem in this constellation: No matter what I tried, Rails complainend “Could not find Image with id=xx for GalleryImageAssignment with id=”, no matter if I added galleryimageids[], passed the image id in the nested image fields or similar. It started working when adding the image id at multiple places, the image, the association and the ids collection but Rails started to add the join records twice. The problem is, that rails tries to update the image attributes through the association, even if the image id is passed. For me it does not make sense, and I found a bug report from 2011 with the same problem that was unfortunately declined. If the id is passed, why can Rails not know, that the record exists and that it has to do an easy find(id) call?

So how to tell rails not to create new records but to update the attributes? Normally it should work by passing the id, but it does not and it is a Rails bug, because it does not make any sense. There is an option for single acceptsnestedattributesfor, not collections. If you pass the option updateonly:true, everything starts working without any hacks or passing ids at various positions.

class ImageAssignment < ActiveRecord::Base
  belongs_to :image
  belongs_to :imageable, polymorphic: true

  accepts_nested_attributes_for :image, update_only: true
end

Maybe it is useful for anybody, for me it definitely was!


Comments