file_column の validation で日本語のメッセージを使えるようにする。

趣旨

ファイルアップロードを劇的に楽にするおなじみ file_column プラグイン。作者の Sebastian Kanthak さんは、現在 Google にお勤めの凄腕ハッカー。写真を見る限りは、なかなかのイケメンである。(日本のハッカーで誰が一番イケメンかなあ)


それはさておき、file_column の validation では、ファイル種別やファイルサイズによるチェックが可能である。しかし、英語でしかメッセージを表示してくれない。そこで、エラーメッセージを自分で設定できるようにハックしてみた。

使い方

モデルクラスで、

file_column :image
  
include FileColumn::Validations
validates_file_format_of :image, :in => ["gif", "png", "jpg"], :message => "のファイル形式は gif, png, jpg だけです。"
validates_filesize_of :image, :in => 100..300.kilobytes,
  :too_large_message => "のサイズが大きすぎます。",
  :too_small_message => "のサイズが小さすぎます。"

のように宣言するだけ。簡単だなー。

ソースコード

これは file_column の lib/validation.rb 全体と対応する。したがって、これを validation.rb の中身全部と取り替えてもよい。あるいは、どこか別の場所に保存しておいて config/environment.rb あたりで、Rails の初期化時に読み込むようにしてもよい。

module FileColumn
  module Validations #:nodoc:
    
    def self.append_features(base)
      super
      base.extend(ClassMethods)
    end

    # This module contains methods to create validations of uploaded files. All methods
    # in this module will be included as class methods into <tt>ActiveRecord::Base</tt>
    # so that you can use them in your models like this:
    #
    #    class Entry < ActiveRecord::Base
    #      file_column :image
    #      validates_filesize_of :image, :in => 0..1.megabyte
    #    end
    module ClassMethods
      EXT_REGEXP = /\.([A-z0-9]+)$/ unless defined?(EXT_REGEXP)
    
      # This validates the file type of one or more file_columns.  A list of file columns
      # should be given followed by an options hash.
      #
      # Required options:
      # * <tt>:in</tt> => list of extensions or mime types. If mime types are used they
      #   will be mapped into an extension via FileColumn::ClassMethods::MIME_EXTENSIONS.
      #
      # Examples:
      #     validates_file_format_of :field, :in => ["gif", "png", "jpg"]
      #     validates_file_format_of :field, :in => ["image/jpeg"]
      def validates_file_format_of(*attrs)
      
        options = attrs.pop if attrs.last.is_a?Hash
        raise ArgumentError, "Please include the :in option." if !options || !options[:in]
        options[:in] = [options[:in]] if options[:in].is_a?String
        message = options[:message] || "is not a valid format." 
        raise ArgumentError, "Invalid value for option :in" unless options[:in].is_a?Array
      
        validates_each(attrs, options) do |record, attr, value|
          unless value.blank?
            mime_extensions = record.send("#{attr}_options")[:mime_extensions]
            extensions = options[:in].map{|o| mime_extensions[o] || o }
            record.errors.add attr, message unless extensions.include?(value.scan(EXT_REGEXP).flatten.first)
          end
        end
      
      end
    
      # This validates the file size of one or more file_columns.  A list of file columns
      # should be given followed by an options hash.
      #
      # Required options:
      # * <tt>:in</tt> => A size range.  Note that you can use ActiveSupport's
      #   numeric extensions for kilobytes, etc.
      #
      # Examples:
      #    validates_filesize_of :field, :in => 0..100.megabytes
      #    validates_filesize_of :field, :in => 15.kilobytes..1.megabyte
      def validates_filesize_of(*attrs)  
      
        options = attrs.pop if attrs.last.is_a?Hash
        raise ArgumentError, "Please include the :in option." if !options || !options[:in]
        raise ArgumentError, "Invalid value for option :in" unless options[:in].is_a?Range
      
        too_small_message = options[:too_small_message] || "is smaller than the allowed size range."
        too_large_message = options[:too_large_message] || "is larger than the allowed size range."
      
        validates_each(attrs, options) do |record, attr, value|
          unless value.blank?
            size = File.size(value)
            record.errors.add attr, too_small_message if size < options[:in].first
            record.errors.add attr, too_large_message if size > options[:in].last
          end
        end
      
      end 

      IMAGE_SIZE_REGEXP = /^(\d+)x(\d+)$/ unless defined?(IMAGE_SIZE_REGEXP)

      # Validates the image size of one or more file_columns.  A list of file columns
      # should be given followed by an options hash. The validation will pass
      # if both image dimensions (rows and columns) are at least as big as
      # given in the <tt>:min</tt> option.
      #
      # Required options:
      # * <tt>:min</tt> => minimum image dimension string, in the format NNxNN
      #   (columns x rows).
      #
      # Example:
      #    validates_image_size :field, :min => "1200x1800"
      #
      # This validation requires RMagick to be installed on your system
      # to check the image's size.
      def validates_image_size(*attrs)      
        options = attrs.pop if attrs.last.is_a?Hash
        raise ArgumentError, "Please include a :min option." if !options || !options[:min]
        minimums = options[:min].scan(IMAGE_SIZE_REGEXP).first.collect{|n| n.to_i} rescue []
        raise ArgumentError, "Invalid value for option :min (should be 'XXxYY')" unless minimums.size == 2
        
        too_small_message =  options[:too_small_message] || "is too small, must be at least %dx%d"
        invalid_message = options[:invalid_message] || "invalid image"

        require 'RMagick'

        validates_each(attrs, options) do |record, attr, value|
          unless value.blank?
            begin
              img = ::Magick::Image::read(value).first
              record.errors.add(attr, invalid_message % [minimums[0], minimums[1]]) if ( img.rows < minimums[1] || img.columns < minimums[0] )
            rescue ::Magick::ImageMagickError
              record.errors.add(attr, invalid_message)
            end
            img = nil
            GC.start
          end
        end
      end
    end
  end
end