ruby-on-rails-5image-uploadingcocoon-gemrails-activestorage

Rails 5.2 Active Storage with Cocoon forms


I want to save some images to a model using a dynamic cocoon form with Active Storage to handle the files.

I have a farmer class that has many apples, the farmer can add multiple images for each of the different kinds of apples through the farmer form.

class Farmer < ActiveRecord::Base
  has_many :apples, inverse_of: :farmer
  accepts_nested_attributes_for :apples, allow_destroy: true,
                                reject_if: :all_blank
end

class Apple < ActiveRecord::Base
  has_many_attached :apple_images
end

Inside the Farmer Controller I have:

class Farmer < ApplicationController


  def update
    @farmer = Farmer.find(params[:farmer_id])

    if @farmer.valid?
      @farmer.update!(farmer_params)
      redirect_to edit_farmer_path(farmer_id: @farmer.id)
    else
      ...
      ...
    end
  end

  private
  def farmer_params
    params.require(:farmer).permit(
      :farmer_name,
      apples_attributes: [
        :id,
        :color,
        :size,
        :_destroy
      ])
  end
end

my view I just added this to my cocoon fields

<div class="form-field">
  <%= f.label :apple_images, "Please upload apple images" %>
  <%= f.file_field :apple_images, multiple: true, data: { validates: {required: {}} } %>
</div>

Now cocoon will save the apple attributes using the accepts_nested_attributes_for call once the farmer object is saved. This is all working just fine until I tried adding the apple_images to the form.

Reading up on the Active Storage readme I see it suggests you should attach the files right after the item has been saved.

You can read the readme here

but in short if you want a single image in the controller do this:

#inside an update method
Current.user.avatar.attach(params.require(:avatar))

or if you want multiple images:

def create
  message = Message.create! params.require(:message).permit(:title, :content)
  message.images.attach(params[:message][:images])
  redirect_to message
end

This seems fairly simple when the image is directly on the model I am saving within the controller.

At first, I thought it may be just as easy as adding apple_images to the params like so:

  def farmer_params
    params.require(:farmer).permit(
      :farmer_name,
      apples_attributes: [
        :id,
        :color,
        :size,
        :apple_images,
        :_destroy
      ])
  end

but this will return an error:

ActiveModel::UnknownAttributeError - unknown attribute 'apple_images' for Apple.:

I am thinking about using an after_save callback on the apple model to attach the images after the apple object is updated / created. Although I am not sure how to achieve this either.

Feeling a little lost, any ideas or suggestions will be greatly appreciated

EDIT

This is what the params look like at the time of update:

 <ActionController::Parameters {"utf8"=>"✓", "_method"=>"patch",
   "farmer"=>{"farmer_name"=>"billy the farmer", "apples_attributes"=>
     {"0"=>{"color"=>"Green", 
            "size"=>"A", 
            "apple_images"=>[#<ActionDispatch::Http::UploadedFile:0x007f9e8aa93168 @tempfile=#<Tempfile:/var/folders/n7/65r5561n44q0w4bdnmw42l880000gn/T/RackMultipart20171211-87415-1m2w7gh.png>, @original_filename="Screen Shot 2017-12-07 at 09.13.28.png", @content_type="image/png", @headers="Content-Disposition: form-data; name=\"farmer[apples_attributes][0][apple_images][]\"; filename=\"Screen Shot 2017-12-07 at 09.13.28.png\"\r\nContent-Type: image/png\r\n">, 
                             #<ActionDispatch::Http::UploadedFile:0x007f9e8aa93118 @tempfile=#<Tempfile:/var/folders/n7/65r5561n44q0w4bdnmw42l880000gn/T/RackMultipart20171211-87415-1gdbax2.jpeg>, @original_filename="WhatsApp Image 2017-12-06 at 1.23.35 PM.jpeg", @content_type="image/jpeg", @headers="Content-Disposition: form-data; name=\"farmer[apples_attributes][0][apple_images][]\"; filename=\"WhatsApp Image 2017-12-06 at 1.23.35 PM.jpeg\"\r\nContent-Type: image/jpeg\r\n">], 
      "_destroy"=>"false", "id"=>"4"}}}, 
     "commit"=>"Next", 
     "controller"=>"farmer/produce", 
     "action"=>"update", 
     "farmer_id"=>"3"} permitted: false>

Solution

  • You should remove the apple_images from the farmer_params (because it is not a known attribute of Apple). But removing that will make sure the images are not saved. This is however, how it is intended to work (a bit weird imho).

    If you check the documentation they explicitly ignore the images attribute and set it separately:

    message = Message.create! params.require(:message).permit(:title, :content)
    message.images.attach(params[:message][:images])
    

    I am not entirely sure how you should solve this in a nested form setting. You could iterate over all apples in the params and try to set the apple_images but that seems very error-prone (how do you match a new apple without id to the one that is saved?).

    You could try adding a method as follows (and keep the apple_images in the permitted params):

    def apple_images=(images)
      self.apple_images.attach(images)
    end 
    

    But not sure if that works before the apple is saved.