legendary_rob
legendary_rob

Reputation: 13002

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>

Upvotes: 2

Views: 3147

Answers (3)

user24422886
user24422886

Reputation: 1

One way is to add files using hidden_field and signed_id as in rails 7 documentation

- f.object.files.each do |image|
 = f.hidden_field :files, multiple: true, value: image.signed_id
=f.polaris_dropzone :files, as: :file, label: 'Files', multiple: true

here is the reference

Upvotes: 0

snowangel
snowangel

Reputation: 3462

Add apple_images as the key to a hash with an empty array, like this:

 params.require(:farmer).permit(
  :farmer_name,
  { apple_images: [] },
  apples_attributes: [
    :id,
    :color,
    :size,
    :_destroy
  ])

This is because when you upload multiple images at once, they're sent in params as an array.

Upvotes: 1

nathanvda
nathanvda

Reputation: 50057

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.

Upvotes: 3

Related Questions