So, I have this web app that I am creating for a friend that helps with managing inventory for eBay auctions.
When a user created a product, my intention was to allow them to upload as many images as they would like to be associated with that product. When they would go to edit the inventory item I would like for them to be able to delete any previously uploaded images and upload new images if they like.
Here’s what my interface looks like.

So I have a good idea that I will probably use attachment_fu to manage the uploading of images. But wait, things are slightly more complicated because I will have one model that manages the product attributes (Products) and then another model that manages the uploaded images (ProductImages).
As usual, Ryan Bates to the rescue with his Railscast “Complex Forms Part 3″. Much of the following code will be familiar if you’ve watched this Railscast; I am just going to demonstrate the usage of attachment_fu with a multi-model setup.
So, within the product new/edit form I have a div defined where my images will be displayed.
........
<p>
Wholesale Price
< %= f.text_field :wholesale_price %>
</p>
<div id="images">
Images
< % @product.product_images.in_groups_of(3) do |group| %>
< %= render :partial => 'product_image', :collection => group %>
<p style="clear: both"/>
< % end %>
</div>
<p style="clear: both"/>
< %= link_to_function image_tag('add.png', :border => 0, :alt => "Add image.") + " Add Image" do |page|
page.insert_html :bottom, :images, :partial => "product_image",
bject => ProductImage.new
end %>
<p>
</p>
My product_image partial looks like this :
< % if product_image != nil %>
< % fields_for "product[image_attributes][]", product_image do |f| %>
< % if product_image.new_record? %>
<div class="newimage">
<p>
< %= f.file_field :uploaded_data, :class => "formField", :index => nil %>
< %= link_to_function "Remove", "$(this).up('.newimage').remove()" %>
</p>
</div>
< % else %>
<div class="image">
< %= image_tag(product_image.public_filename(:stamp), :border => 0) %>
< %= f.file_field :uploaded_data , :index => nil, :style => 'display:none' %>
< %= link_to_function image_tag('cancel.png', :border => 0, :alt => "Delete image."), "mark_for_destroy(this)" %>
< %= f.hidden_field :id, :index => nil %>
< %= f.hidden_field :should_destroy, :index => nil, :class => 'should_destroy' %>
</div>
< % end %>
< % end %>
< % end %>
Areas of interest : I’m using the fields_for form helper because our images are part of another model. There’s a set of empty brackets after the definition
product[image_attributes][]
to tell Rails that we’re passing an array (in this case multiple images).
The call to the mark_for_destroy method. This allows me to directly hide the display of the image and mark a form flag that tells the system to destroy it once the form has been submitted. This technique is outlined in Ryan’s railscast.
In application.js
function mark_for_destroy(element) {
$(element).next('.should_destroy').value = 1;
$(element).up('.image').hide();
}
I have a product_image model that manages the attachments using attachment_fu. It has an attribute should_destroy which allows an image to be marked for deletion from the form.
class ProductImage < ActiveRecord::Base
has_attachment :content_type => :image,
:storage => :file_system,
:max_size => 2000.kilobytes,
:resize_to => '320x200>',
:thumbnails => { :thumb => '150x150>', :stamp => '75x75>' },
:processor => :ImageScience
validates_as_attachment
attr_accessor :should_destroy
def should_destroy?
should_destroy.to_i == 1
end
end
For the products model, we want to setup a virtual attribute called image_attributes (remember the array of images that we defined in our form above?) that will handle the saving/deletion of product images.
class Product < ActiveRecord::Base
belongs_to :vendor
belongs_to :product_status
belongs_to :ebay_category
has_many :product_images, :dependent => :destroy
after_update :save_images
def image_attributes=(image_attributes)
image_attributes.each do |attributes|
if attributes[:id].blank?
product_images.build(attributes)
else
product_image = product_images.detect { |t| t.id == attributes[:id].to_i}
product_image.attributes = attributes
end
end
end
def save_images
product_images.each do |image|
if image.should_destroy?
image.destroy
else
image.save(false)
end
end
end
end
June 19th, 2008 at 5:56 pm
man…thank you so much for this…really
June 19th, 2008 at 6:32 pm
I came with this error while trying to update a file.
undefined method `uploaded_data=’ for #
Any idea?
thanks
June 19th, 2008 at 9:12 pm
Great! it worked…thanks for this again…
June 22nd, 2008 at 2:29 pm
Got it to work and all, but now I’m having problems displaying a thumbnail for a given product. Like just getting the first image associated and displaying it’s thumbnail.
October 28th, 2008 at 1:21 pm
I would love to hear how you tackled validation with this approach? In my case I mimic your strategy pretty close but I put the “attachable” behavior into a module instead which I can reuse.
http://pastie.textmate.org/private/l4h8gifviowysihp32q
My only issue is I’m not able to show full error messages, and just that the association is invalid. IE, “Attachments is invalid”
November 13th, 2008 at 9:24 am
Everything works fine, I just have a small problem, I my Photo model (Product_image) has :user_id, so I added in the
but after executing the save_images my photo :user_id is null, does not update.
November 13th, 2008 at 9:41 am
sorry, I meant that other Product_Image attributes are not updated
September 17th, 2009 at 11:18 am
When you add new image in edit page, the last image will be replaced.
March 9th, 2010 at 10:30 am
I had got a desire to start my commerce, nevertheless I did not have enough amount of cash to do this. Thank God my close fellow advised to utilize the loan. So I received the sba loan and made real my dream.
May 30th, 2010 at 6:46 pm
I’m having problems displaying a thumbnail for a given product. Like just getting the first image associated and displaying it’s thumbnail.