Roll your own attachment_fu validations

Lots of folks use attachment_fu to handle image uploads. It handles all the dirty work of uploading files, integrates with Amazon S3, and even supports all the different image libraries.

Which is awesome... but it kinda sucks at validating files. Take a pretty typical Photo class:

class Photo < ActiveRecord::Base
  has_attachment :content_type => :image, 
                 :storage => :file_system, 
                 :max_size => 5.megabytes,
                 :resize_to => '320x200>',
                 :thumbnails => { :thumb => '100x100>', :widget => "262x262>" },
                 :processor => MiniMagick
  validates_as_attachment

end

What happens when you forget to select a file before uploading?

  • Content type can't be blank
  • Content type is not included in the list
  • Size can't be blank
  • Size is not included in the list
  • Filename can't be blank

So, let's try uploading a Word document:

  • Content type is not included in the list

Or a file that's too big:

  • Size is not included in the list

Let's try it again, but in English:

class Photo < ActiveRecord::Base
  has_attachment :content_type => :image, 
                 :storage => :file_system, 
                 :max_size => 5.megabytes,
                 :resize_to => '320x200>',
                 :thumbnails => { :thumb => '100x100>', :widget => "262x262>" },
                 :processor => MiniMagick
  
  def validate
    errors.add_to_base("You must choose a file to upload") unless self.filename
    
    unless self.filename == nil
      
      # Images should only be GIF, JPEG, or PNG
      [:content_type].each do |attr_name|
        enum = attachment_options[attr_name]
        unless enum.nil? || enum.include?(send(attr_name))
          errors.add_to_base("You can only upload images (GIF, JPEG, or PNG)")
        end
      end
      
      # Images should be less than 5 MB
      [:size].each do |attr_name|
        enum = attachment_options[attr_name]
        unless enum.nil? || enum.include?(send(attr_name))
          errors.add_to_base("Images should be smaller than 5 MB in size")
        end
      end
        
    end
  end

end

To get started, let's drop the validates_as_attachment method. Or, more specifically, try extracting its useful parts into something more useful.

Since we're dropping attachment_fu's validations, we need to roll our own validations for the Photo class. You can do this by declaring a validate method in your class, and adding appropriate logic.

We can also use errors.add_to_base to get total control over your validation messages. By default, Rails pair the model attribute name with an error message ("person" + "is missing an ear"), but sometimes this can lead to awkward phrases ("person is empty").

You'll need to custom this for your application, of course, but this post should give you some examples.

(I'm planning to follow this blog post up with a series of posts about improving validation messages.)

November 27, 2007

Comments

Martijn November 29, 2007

Very interesting. Looking forward to those follow-ups.

Tim Lucas December 03, 2007

Thanks Pat! I got off my ass and improved my validations. I just posted my stab at a plugin for just this, initially based off your code. I've also posted it to the attachment_fu google group for comment.

rick March 03, 2008

It was done that way on purpose so you could do cool stuff like this. I figure it's easier to let you define this stuff in Rails, rather than coming up with my own slick DSL that's constantly being updated for everyone's edge cases.

Patrick Crowley March 26, 2008

Cool, Rick. It's always nice to have options. :)

Darren April 18, 2008

Sweet! This is a nice tutorial for my beginner skills in Ruby. Got my custom errors messages for attachment_fu looking hot!

Luca G.Soave May 12, 2008

Hi Patrick,

Useful article here, as usual. I'm just moving first steps about attachment_fu, looking for a way to upload ":other" files type ... say opml files, do you have some exaples to manage something like :

has_attachment :content_type => ['text/x-opml', 'application/xml', 'text/xml', 'text/html', 'text/plain'], .... and eventually the ":storage => :db_file" way ?

Thanks in advance.
lgs

New comments are disabled right now. Once we finish migrating this blog, we'll restore them.