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>
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.