Commit 2784b380 by jyurek

Huge refactoring.

git-svn-id: https://svn.thoughtbot.com/plugins/paperclip/trunk@390 7bbfaf0e-4d1d-0410-9690-a8bb5f8ef2aa
parent c77ba25e
......@@ -11,7 +11,7 @@ See the documentation for the +has_attached_file+ method for options.
In your model:
class User < ActiveRecord::Base
has_attached_file :avatar, :thumbnails => { :medium => "300x300>", :thumb => "100x100>" }
has_attached_file :avatar, :styles => { :medium => "300x300>", :thumb => "100x100>" }
end
In your edit and new views:
......@@ -23,12 +23,11 @@ In your edit and new views:
In your controller:
def create
@user = User.create( params[:user][:avatar] )
@user = User.create( params[:user] )
end
In your show view:
<%= image_tag @user.avatar.url %>
<%= image_tag @user.avatar.url(:original) %>
<%= image_tag @user.avatar.url(:medium) %>
<%= image_tag @user.avatar.url(:thumb) %>
# Install hook code here
......@@ -5,7 +5,7 @@
# columns to your table.
#
# Author:: Jon Yurek
# Copyright:: Copyright (c) 2007 thoughtbot, inc.
# Copyright:: Copyright (c) 2008 thoughtbot, inc.
# License:: Distrbutes under the same terms as Ruby
#
# Paperclip defines an attachment as any file, though it makes special considerations
......@@ -25,25 +25,22 @@
#
# See the +has_attached_file+ documentation for more details.
require 'paperclip/attachment_definition'
require 'paperclip/attachment'
require 'paperclip/thumbnail'
require 'paperclip/upfile'
require 'paperclip/storage/filesystem'
require 'paperclip/storage/s3'
require 'paperclip/iostream'
require 'paperclip/geometry'
require 'paperclip/thumbnail'
require 'paperclip/attachment'
module Paperclip
class << self
# Provides configurability to Paperclip. There are a number of options available, such as:
# * whiny_deletes: Will raise an error if Paperclip is unable to delete an attachment. Defaults to false.
# * whiny_thumbnails: Will raise an error if Paperclip cannot process thumbnails of an uploaded image. Defaults to true.
# * image_magick_path: Defines the path at which to find the +convert+ and +identify+ programs if they are
# not visible to Rails the system's search path. Defaults to nil, which uses the first executable found
# in the search path.
# * whiny_thumbnails: Will raise an error if Paperclip cannot process thumbnails of
# an uploaded image. Defaults to true.
# * image_magick_path: Defines the path at which to find the +convert+ and +identify+
# programs if they are not visible to Rails the system's search path. Defaults to
# nil, which uses the first executable found in the search path.
def options
@options ||= {
:whiny_deletes => false,
:whiny_thumbnails => true,
:image_magick_path => nil
}
......@@ -53,39 +50,48 @@ module Paperclip
path = [options[:image_magick_path], command].compact
File.join(*path)
end
def included base #:nodoc:
base.extend ClassMethods
end
end
class PaperclipError < StandardError #:nodoc:
end
module ClassMethods
attr_reader :attachment_definitions
# +has_attached_file+ gives the class it is called on an attribute that maps to a file. This
# is typically a file stored somewhere on the filesystem and has been uploaded by a user. The
# attribute returns a Paperclip::Attachment object which handles the management of that file.
# The intent is to make the attachment as much like a normal attribute. The thumbnails will be
# created when the new file is assigned, but they will *not* be saved until +save+ is called on
# the record. Likewise, if the attribute is set to +nil+ or +Paperclip::Attachment#destroy+
# is called on it, the attachment will *not* be deleted until +save+ is called. See the
# Paperclip::Attachment documentation for more specifics.
# There are a number of options you can set to change the behavior of a Paperclip attachment:
# * +url+: The full URL of where the attachment is publically accessible. This can just as easily
# point to a directory served directly through Apache as it can to an action that can control
# permissions. You can specify the full domain and path, but usually just an absolute path is
# sufficient. The leading slash must be included manually for absolute paths. The default value
# is "/:class/:attachment/:id/:style_:filename". See
# is typically a file stored somewhere on the filesystem and has been uploaded by a user.
# The attribute returns a Paperclip::Attachment object which handles the management of
# that file. The intent is to make the attachment as much like a normal attribute. The
# thumbnails will be created when the new file is assigned, but they will *not* be saved
# until +save+ is called on the record. Likewise, if the attribute is set to +nil+ is
# called on it, the attachment will *not* be deleted until +save+ is called. See the
# Paperclip::Attachment documentation for more specifics. There are a number of options
# you can set to change the behavior of a Paperclip attachment:
# * +url+: The full URL of where the attachment is publically accessible. This can just
# as easily point to a directory served directly through Apache as it can to an action
# that can control permissions. You can specify the full domain and path, but usually
# just an absolute path is sufficient. The leading slash must be included manually for
# absolute paths. The default value is "/:class/:attachment/:id/:style_:filename". See
# Paperclip::Attachment#interpolate for more information on variable interpolaton.
# :url => "/:attachment/:id/:style_:name"
# :url => "/:attachment/:id/:style_:basename:extension"
# :url => "http://some.other.host/stuff/:class/:id_:extension"
# * +missing_url+: The URL that will be returned if there is no attachment assigned. This field
# is interpolated just as the url is. The default value is "/:class/:attachment/missing_:style.png"
# * +missing_url+: The URL that will be returned if there is no attachment assigned.
# This field is interpolated just as the url is. The default value is
# "/:class/:attachment/missing_:style.png"
# has_attached_file :avatar, :missing_url => "/images/default_:style_avatar.png"
# User.new.avatar_url(:small) # => "/images/default_small_avatar.png"
# * +styles+: A hash of thumbnail styles and their geometries. You can find more about geometry strings
# at the ImageMagick website (http://www.imagemagick.org/script/command-line-options.php#resize). Paperclip
# also adds the "#" option (e.g. "50x50#"), which will resize the image to fit maximally inside
# the dimensions and then crop the rest off (weighted at the center). The default value is
# to generate no thumbnails.
# * +default_style+: The thumbnail style that will be used by default URLs. Defaults to +original+.
# * +styles+: A hash of thumbnail styles and their geometries. You can find more about
# geometry strings at the ImageMagick website
# (http://www.imagemagick.org/script/command-line-options.php#resize). Paperclip
# also adds the "#" option (e.g. "50x50#"), which will resize the image to fit maximally
# inside the dimensions and then crop the rest off (weighted at the center). The
# default value is to generate no thumbnails.
# * +default_style+: The thumbnail style that will be used by default URLs.
# Defaults to +original+.
# has_attached_file :avatar, :styles => { :normal => "100x100#" },
# :default_style => :normal
# user.avatar.url # => "/avatars/23/normal_me.png"
......@@ -93,94 +99,96 @@ module Paperclip
# with the value of the +url+ option to allow files to be saved into a place where Apache
# can serve them without hitting your app. Defaults to
# ":rails_root/public/:class/:attachment/:id/:style_:filename".
# By default this places the files in the app's public directory which can be served directly.
# If you are using capistrano for deployment, a good idea would be to make a symlink to the
# capistrano-created system directory from inside your app's public directory.
# By default this places the files in the app's public directory which can be served
# directly. If you are using capistrano for deployment, a good idea would be to
# make a symlink to the capistrano-created system directory from inside your app's
# public directory.
# See Paperclip::Attachment#interpolate for more information on variable interpolaton.
# :path => "/var/app/attachments/:class/:id/:style/:filename"
# * +whiny_thumbnails+: Will raise an error if Paperclip cannot process thumbnails of an
# uploaded image. This will ovrride the global setting for this attachment. Defaults to true.
# * +delete_on_destroy+: When records are deleted, the attachment that goes with it is also deleted. Set
# this to +false+ to prevent the file from being deleted. Defaults to +true+.
# * +storage+: Specifies the storage method, either :filesystem or :s3. S3 storage is currently
# experimental and should not be used. Defaults to :filesystem.
def has_attached_file *attachment_names
options = attachment_names.last.is_a?(Hash) ? attachment_names.pop : {}
# uploaded image. This will ovrride the global setting for this attachment.
# Defaults to true.
def has_attached_file name, options = {}
include InstanceMethods
after_save :save_attached_files
before_destroy :destroy_attached_files
@attachment_definitions ||= {}
@attachment_names ||= []
@attachment_names += attachment_names
@attachment_definitions ||= {}
@attachment_definitions[name] = {:validations => []}.merge(options)
attachment_names.each do |aname|
whine_about_columns_for aname
@attachment_definitions[aname] = AttachmentDefinition.new(aname, options)
after_save :save_attached_files
before_destroy :destroy_attached_files
define_method aname do
attachment_for(aname)
end
define_method name do |*args|
a = attachment_for(name)
(args.length > 0) ? a.to_s(args.first) : a
end
define_method "#{aname}=" do |uploaded_file|
attachment_for(aname).assign uploaded_file
end
define_method "#{name}=" do |file|
attachment_for(name).assign(file)
end
end
# Returns an array of all the attachments defined on this class.
def attached_files
@attachment_names
end
define_method "#{name}?" do
! attachment_for(name).file.nil?
end
# Returns a AttachmentDefinition for the given attachment
def attachment_definition_for attachment
@attachment_definitions[attachment]
validates_each(name) do |record, attr, value|
value.send(:flush_errors)
end
end
# Adds errors if the attachments you specify are either missing or had errors on them.
# Essentially, acts like validates_presence_of for attachments.
def validates_attached_file *attachment_names
validates_each *attachment_names do |record, name, attachment|
attachment.errors.each do |error|
record.errors.add(name, error)
# Places ActiveRecord-style validations on the size of the file assigned. The
# possible options are :in, which takes a Range of bytes (i.e. +1..1.megabyte+),
# :less_than, which is equivalent to :in => 0..options[:less_than], and
# :greater_than, which is equivalent to :in => options[:greater_than]..Infinity
def validates_attachment_size name, options = {}
@attachment_definitions[name][:validations] << lambda do |attachment, instance|
unless options[:greater_than].nil?
options[:in] = (options[:greater_than]..(1/0)) # 1/0 => Infinity
end
unless options[:less_than].nil?
options[:in] = (0..options[:less_than])
end
unless options[:in].include? instance[:"#{name}_file_size"].to_i
"file size is not between #{options[:in].first} and #{options[:in].last} bytes."
end
end
end
# Throws errors if the model does not contain the necessary columns.
def whine_about_columns_for attachment #:nodoc:
unless column_names.include?("#{attachment}_file_name") && column_names.include?("#{attachment}_content_type")
error = "Class #{name} does not have the necessary columns to have an attachment named #{attachment}. " +
"(#{attachment}_file_name and #{attachment}_content_type)"
raise PaperclipError, error
# Places ActiveRecord-style validations on the presence of a file.
def validates_attachment_presence name
@attachment_definitions[name][:validations] << lambda do |attachment, instance|
if attachment.file.nil? || !File.exist?(attachment.file.path)
"must be set."
end
end
end
end
module InstanceMethods #:nodoc:
def attachment_for name
@attachments ||= {}
@attachments[name] ||= Attachment.new(self, name, self.class.attachment_definition_for(name))
@attachments[name] ||= Attachment.new(name, self, self.class.attachment_definitions[name])
end
def each_attachment
self.class.attached_files.each do |name|
self.class.attachment_definitions.each do |name, definition|
yield(name, attachment_for(name))
end
end
def save_attached_files
each_attachment do |name, attachment|
attachment.save
attachment.send(:flush_writes)
attachment.send(:flush_deletes)
end
end
def destroy_attached_files
each_attachment do |name, attachment|
attachment.destroy!
attachment.send(:queue_existing_for_delete)
attachment.send(:flush_deletes)
end
end
end
end
module Paperclip
$LOAD_PATH << File.dirname(__FILE__)
require 'iostream'
require 'upfile'
require 'thumbnail'
require 'geometry'
# == Attachment
# Handles all the file management for the attachment, including saving, loading, presenting URLs,
# and database storage.
module Paperclip
class Attachment
attr_reader :name, :instance, :file, :styles, :default_style
# Creates an Attachment object. +name+ is the name of the attachment, +instance+ is the
# ActiveRecord object instance it's attached to, and +options+ is the same as the hash
# passed to +has_attached_file+.
def initialize name, instance, options
@name = name
@instance = instance
@url = options[:url] ||
"/:attachment/:id/:style/:basename.:extension"
@path = options[:path] ||
":attachment/:id/:style/:basename.:extension"
@styles = options[:styles] || {}
@default_url = options[:default_url] || "/:attachment/:style/missing.png"
@validations = options[:validations] || []
@default_style = options[:default_style] || :original
@queued_for_delete = []
@processed_files = {}
@errors = []
@validation_errors = nil
@dirty = false
@file = File.new(path) if original_filename && File.exists?(path)
end
# What gets called when you call instance.attachment = File. It clears errors,
# assigns attributes, processes the file, and runs validations. It also queues up
# the previous file for deletion, to be flushed away on #save of its host.
def assign uploaded_file
queue_existing_for_delete
@errors = []
@validation_errors = nil
attr_reader :name, :instance, :original_filename, :content_type, :original_file_size, :definition, :errors
return nil unless valid_file?(uploaded_file)
def initialize active_record, name, definition
@instance = active_record
@definition = definition
@name = name
@errors = []
@file = uploaded_file.to_tempfile
@instance[:"#{@name}_file_name"] = uploaded_file.original_filename
@instance[:"#{@name}_content_type"] = uploaded_file.content_type
@instance[:"#{@name}_file_size"] = uploaded_file.size
clear_files
@dirty = true
self.original_filename = @instance["#{name}_file_name"]
self.content_type = @instance["#{name}_content_type"]
self.original_file_size = @instance["#{name}_file_size"]
storage_module = Paperclip::Storage.const_get((definition.storage || :filesystem).to_s.camelize)
self.extend(storage_module)
post_process
ensure
validate
end
# Sets the file managed by this instance. It also creates the thumbnails if the attachment is an image.
def assign uploaded_file
return destroy if uploaded_file.nil?
return unless is_a_file? uploaded_file
self.original_filename = sanitize_filename(uploaded_file.original_filename)
self.content_type = uploaded_file.content_type
self.original_file_size = uploaded_file.size
self[:original] = uploaded_file.read
@dirty = true
@delete_on_save = false
convert( self[:original] )
# Returns the public URL of the attachment, with a given style. Note that this
# does not necessarily need to point to a file that your web server can access
# and can point to an action in your app, if you need fine grained security.
# This is not recommended if you don't need the security, however, for
# performance reasons.
def url style = nil
interpolate(@url, style) || interpolate(@default_url, style)
end
def [](style) #:nodoc:
@files[style]
# Alias to +url+
def to_s style = nil
url(style)
end
def []=(style, data) #:nodoc:
@dirty = true
@files[style] = data
# Returns true if there are any errors on this attachment.
def valid?
errors.length == 0
end
def clear_files #:nodoc:
@files = {}
definition.styles.each{|style, geo| self[style] = nil }
@dirty = false
# Returns an array containing the errors on this attachment.
def errors
@errors.compact.uniq
end
# Iterates over the files that are stored in memory and hands them to the
# supplied block. If no assignment has happened since either the object
# was instantiated or the last time it was saved, +nil+ will be passed as
# the data argument.
def each_unsaved
@files.each do |style, data|
yield( style, data ) if data
end
end
def styles
@files.keys
end
# Returns true if the attachment has been assigned and not saved.
# Returns true if there are changes that need to be saved.
def dirty?
@dirty
end
# Runs any validations that have been defined on the attachment.
def valid?
definition.validations.each do |validation, constraints|
send("validate_#{validation}", *constraints)
end
errors.uniq!.empty?
end
# Writes (or deletes, if +nil+) the attachment. This is called automatically
# when the active record is saved; you do not need to call this yourself.
# Saves the file, if there are no errors. If there are, it flushes them to
# the instance's errors and returns false, cancelling the save.
def save
write_attachment if dirty?
delete_attachment if @delete_on_save
@delete_on_save = false
clear_files
end
# Queues up the attachment for destruction, but does not actually delete.
# The attachment will be deleted when the record is saved.
def destroy(complain = false)
returning true do
@delete_on_save = true
@complain_on_delete = complain
self.original_filename = nil
self.content_type = nil
self.original_file_size = nil
clear_files
if valid?
flush_deletes
flush_writes
true
else
flush_errors
false
end
end
# Immediately destroys the attachment. Typically called as an ActiveRecord
# callback on destroy. You shold not need to call this.
def destroy!
delete_attachment if definition.delete_on_destroy
end
# Returns the public-facing URL of the attachment. If this record has not
# been saved or does not have an attachment, this method will return the
# "missing" url, which can be used to supply a default. This is what should
# be supplied to the +image_tag+ helper.
def url style = nil
style ||= definition.default_style
pattern = if original_filename && instance.id
definition.url
else
definition.missing_url
# Returns an +IO+ representing the data of the file assigned to the given
# style. Useful for streaming with +send_file+.
def to_io style = nil
begin
@processed_files[style] || File.new(path(style))
rescue Errno::ENOENT
nil
end
interpolate( style, pattern )
end
# Returns the data contained by the attachment of a particular style. This
# should be used if you need to restrict permissions internally to the app.
def read style = nil
style ||= definition.default_style
self[style] ? self[style] : read_attachment(style)
# Returns the name of the file as originally assigned, and as lives in the
# <attachment>_file_name attribute of the model.
def original_filename
instance[:"#{name}_file_name"]
end
# Sets errors if there must be an attachment but isn't.
def validate_existence *constraints
definition.styles.keys.each do |style|
errors << "requires a valid #{style} file." unless attachment_exists?(style)
end
# A hash of procs that are run during the interpolation of a path or url.
# A variable of the format :name will be replaced with the return value of
# the proc named ":name". Each lambda takes the attachment and the current
# style as arguments. This hash can be added to with your own proc if
# necessary.
def self.interpolations
@interpolations ||= {
:rails_root => lambda{|attachment,style| RAILS_ROOT },
:class => lambda{|attachment,style| attachment.instance.class.to_s },
:basename => lambda do |attachment,style|
attachment.original_filename.gsub(/\.(.*?)$/, "")
end,
:extension => lambda do |attachment,style|
((style = attachment.styles[style]) && style.last) ||
File.extname(attachment.original_filename).gsub(/^\./, "")
end,
:id => lambda{|attachment,style| attachment.instance.id },
:attachment => lambda{|attachment,style| attachment.name },
:style => lambda{|attachment,style| style || attachment.default_style },
}
end
# Sets errors if the file does not meet the file size constraints.
def validate_size *constraints
errors << "file too large. Must be under #{constraints.last} bytes." if original_file_size > constraints.last
errors << "file too small. Must be over #{constraints.first} bytes." if original_file_size <= constraints.first
end
private
# Returns true if all the files exist.
def exists?(style)
style ||= definition.default_style
attachment_exists?(style)
def valid_file? file #:nodoc:
file.respond_to?(:original_filename) && file.respond_to?(:content_type)
end
# Generates the thumbnails from the data supplied. Following this call, the data will
# be available from for_attached_files.
def convert data
begin
definition.styles.each do |style, geometry|
self[style] = Thumbnail.make(geometry, data, definition.whiny_thumbnails)
end
rescue PaperclipError => e
errors << e.message
clear_files
self[:original] = data
def validate #:nodoc:
unless @validation_errors
@validation_errors = @validations.collect do |v|
v.call(self, instance)
end.flatten.compact.uniq
@errors += @validation_errors
end
end
# Returns a hash of procs that will perform the various interpolations for
# the path, url, and missing_url attachment options. The procs are used as
# arguments to gsub!, so the used will be replaced with the return value
# of the proc. You can add to this list by assigning to the hash:
# Paperclip::Attachment.interpolations[:content_type] = lambda{|style, attachment| attachment.content_type }
# ...
# attachment.interpolate("original", ":content_type")
# # => "image/jpeg"
def self.interpolations
@interpolations ||= {
:rails_root => lambda{|style, atch| RAILS_ROOT },
:id => lambda{|style, atch| atch.instance.id },
:class => lambda{|style, atch| atch.instance.class.to_s.underscore.pluralize },
:style => lambda{|style, atch| style.to_s },
:attachment => lambda{|style, atch| atch.name.to_s.pluralize },
:filename => lambda{|style, atch| atch.original_filename },
:basename => lambda{|style, atch| atch.original_filename.gsub(/\..*$/, "") },
:extension => lambda{|style, atch| atch.original_filename.gsub(/^.*\./, "") }
}
def post_process #:nodoc:
return nil if @file.nil?
@styles.each do |name, args|
begin
dimensions, format = [args, nil].flatten[0..1]
@styles[name] = [dimensions, format]
@processed_files[name] = Thumbnail.make(self.file,
dimensions,
format,
@whiny_thumbnails)
rescue Errno::ENOENT => e
@errors << "could not be processed because the file does not exist."
rescue PaperclipError => e
@errors << e.message
end
end
@processed_files[:original] = @file
end
# Searches for patterns in +source+ string supplied and replaces them with values
# returned by the procs in the interpolations hash.
def interpolate style, source
returning source.dup do |s|
Attachment.interpolations.each do |key, proc|
s.gsub!(/:#{key}/) do
proc.call(style, self) rescue ":#{key}"
end
def interpolate pattern, style = nil #:nodoc:
style ||= @default_style
pattern = pattern.dup
self.class.interpolations.each do |tag, l|
pattern.gsub!(/:#{tag}/) do |match|
l.call( self, style )
end
end
pattern.gsub(%r{/+}, "/")
end
# Sets the *_file_name column on the activerecord for this attachment
def original_filename= new_name
instance["#{name}_file_name"] = @original_filename = new_name
end
# Sets the *_content_type column on the activerecord for this attachment
def content_type= new_type
instance["#{name}_content_type"] = @content_type = new_type
def path style = nil #:nodoc:
interpolate(@path, style)
end
# Sets the *_file_size column on the activerecord for this attachment
def original_file_size= new_size
instance["#{name}_file_size"] = @original_file_size = new_size
def queue_existing_for_delete #:nodoc:
@queued_for_delete += @processed_files.values
@file = nil
@processed_files = {}
@instance[:"#{@name}_file_name"] = nil
@instance[:"#{@name}_content_type"] = nil
@instance[:"#{@name}_file_size"] = nil
end
def to_s
url
def flush_errors #:nodoc:
@errors.each do |error|
instance.errors.add(name, error)
end
end
protected
def is_a_file? data #:nodoc:
[:content_type, :original_filename, :read].map do |meth|
data.respond_to? meth
end.all?
def flush_writes #:nodoc:
@processed_files.each do |style, file|
FileUtils.mkdir_p( File.dirname(path(style)) )
@processed_files[style] = file.stream_to(path(style)) unless file.path == path(style)
end
@file = @processed_files[nil]
end
def sanitize_filename filename #:nodoc:
File.basename(filename).gsub(/[^\w\.\_\-]/,'_')
def flush_deletes #:nodoc:
@queued_for_delete.compact.each do |file|
begin
FileUtils.rm(file.path)
rescue Errno::ENOENT => e
# ignore them
end
end
@queued_for_delete = []
end
end
end
module Paperclip
# Defines the geometry of an image.
class Geometry
attr_accessor :height, :width, :modifier
# Gives a Geometry representing the given height and width
def initialize width = nil, height = nil, modifier = nil
height = nil if height == ""
width = nil if width == ""
@height = (height || width).to_f
@width = (width || height).to_f
@modifier = modifier
end
# Uses ImageMagick to determing the dimensions of a file, passed in as either a
# File or path.
def self.from_file file
file = file.path if file.respond_to? "path"
parse(`#{Paperclip.path_for_command('identify')} "#{file}" 2>/dev/null`) ||
raise(Errno::ENOENT, file)
end
# Parses a "WxH" formatted string, where W is the width and H is the height.
def self.parse string
if match = (string && string.match(/\b(\d*)x(\d*)([><\#\@\%^!])?\b/))
Geometry.new(*match[1,3])
end
end
# True if the dimensions represent a square
def square?
height == width
end
# True if the dimensions represent a horizontal rectangle
def horizontal?
height < width
end
# True if the dimensions represent a vertical rectangle
def vertical?
height > width
end
# The aspect ratio of the dimensions.
def aspect
width / height
end
# Returns the larger of the two dimensions
def larger
[height, width].max
end
# Returns the smaller of the two dimensions
def smaller
[height, width].min
end
# Returns the width and height in a format suitable to be passed to Geometry.parse
def to_s
"%dx%d" % [width, height]
end
# Same as to_s
def inspect
to_s
end
# Returns the scaling and cropping geometries (in string-based ImageMagick format)
# neccessary to transform this Geometry into the Geometry given. If crop is true,
# then it is assumed the destination Geometry will be the exact final resolution.
# In this case, the source Geometry is scaled so that an image containing the
# destination Geometry would be completely filled by the source image, and any
# overhanging image would be cropped. Useful for square thumbnail images. The cropping
# is weighted at the center of the Geometry.
def transformation_to dst, crop = false
ratio = Geometry.new( dst.width / self.width, dst.height / self.height )
if crop
scale_geometry, scale = if ratio.horizontal? || ratio.square?
[ "%dx" % dst.width, ratio.width ]
else
[ "x%d" % dst.height, ratio.height ]
end
crop_geometry = if ratio.horizontal? || ratio.square?
"%dx%d+%d+%d" % [ dst.width, dst.height, 0, (self.height * scale - dst.height) / 2 ]
else
"%dx%d+%d+%d" % [ dst.width, dst.height, (self.width * scale - dst.width) / 2, 0 ]
end
else
scale_geometry = dst.to_s
end
[ scale_geometry, crop_geometry ]
end
end
end
# Provides method that can be included on File-type objects (IO, StringIO, Tempfile, etc) to allow stream copying
# and Tempfile conversion.
module IOStream
# Returns a Tempfile containing the contents of the readable object.
def to_tempfile
tempfile = Tempfile.new("stream")
self.stream_to(tempfile)
end
# Copies one read-able object from one place to another in blocks, obviating the need to load
# the whole thing into memory. Defaults to 8k blocks. If this module is included in both
# both StringIO and Tempfile, then either can have its data copied anywhere else without typing
# worries or memory overhead worries. Returns a File if a String is passed in as the destination
# and returns the IO or Tempfile as passed in if one is sent as the destination.
def stream_to path_or_file, in_blocks_of = 8192
dstio = case path_or_file
when String then File.new(path_or_file, "w+")
when IO then path_or_file
when Tempfile then path_or_file
end
buffer = ""
self.rewind
while self.read(in_blocks_of, buffer) do
dstio.write(buffer)
end
dstio.rewind
dstio
end
end
class IO
include IOStream
end
%w( Tempfile StringIO ).each do |klass|
if Object.const_defined? klass
Object.const_get(klass).class_eval do
include IOStream
end
end
end
module Paperclip
module Storage
module Filesystem
def write_attachment
ensure_directories
each_unsaved do |style, data|
File.open( file_name(style), "w" ) do |file|
file.rewind
file.write(data)
end
end
end
def read_attachment style = nil
IO.read(file_name(style))
end
def delete_attachment complain = false
styles.each do |style|
file_path = file_name(style)
begin
FileUtils.rm file_path if file_path
rescue SystemCallError => e
raise PaperclipError, "could not be deleted." if Paperclip.options[:whiny_deletes] || complain
end
end
end
def attachment_exists? style = nil
File.exists?( file_name(style) )
end
def file_name style = nil
style ||= definition.default_style
interpolate( style, definition.path )
end
def ensure_directories
each_unsaved do |style, file|
dirname = File.dirname( file_name(style) )
FileUtils.mkdir_p dirname
end
end
end
end
end
\ No newline at end of file
module Paperclip
module Storage
module S3
def self.extended(base)
Paperclip.options[:s3] ||= {}
Paperclip::Attachment.interpolations[:bucket] = lambda{|style, atch| atch.definition.bucket }
access_key, secret_key = credentials
require 'aws/s3'
AWS::S3::Base.establish_connection!(
:access_key_id => access_key,
:secret_access_key => secret_key,
:persistent => Paperclip.options[:s3][:persistent] || true
)
class << base
alias_method_chain :url, :s3
end
end
def self.credentials
if credentials_file
creds = YAML.load_file(credentials_file)
creds = creds[RAILS_ENV] || creds if Object.const_defined?("RAILS_ENV")
[ creds['access_key_id'], creds['secret_access_key'] ]
else
[ Paperclip.options[:s3][:access_key_id], Paperclip.options[:s3][:secret_access_key] ]
end
end
def self.credentials_file
@file ||= [ Paperclip.options[:s3][:credentials_file], File.join(RAILS_ROOT, "config", "s3.yml") ].compact.find do |f|
File.exists?(f)
end
end
def url_with_s3 style = nil
http_host = definition.s3_host || "http://s3.amazonaws.com"
"#{http_host}/#{bucket}#{url_without_s3(style)}"
end
def file_name style = nil
style ||= definition.default_style
interpolate( style, definition.url )
end
def bucket
definition.bucket
end
def ensure_bucket
begin
AWS::S3::Bucket.create(bucket)
bucket
rescue AWS::S3::S3Exception => e
raise Paperclip::PaperclipError, "You are not allowed access to the bucket '#{bucket_name}'."
end
end
def stream style = nil, &block
AWS::S3::S3Object.stream( file_name(style), bucket, &block )
end
# These four methods are the primary interface for the storage module.
# Everything above this is support for these methods.
def attachment_exists? style = nil
AWS::S3::S3Object.exists?( file_name(style), bucket )
end
def write_attachment
ensure_bucket
each_unsaved do |style, data|
AWS::S3::S3Object.store( file_name(style), data, bucket, :access => definition.s3_access || :public_read )
end
end
def read_attachment style = nil
AWS::S3::S3Object.value( file_name(style), bucket )
end
def delete_attachment complain = false
styles.each do |style, data|
AWS::S3::S3Object.delete( file_name(style), bucket )
end
end
end
end
end
\ No newline at end of file
module Paperclip
class Thumbnail
class Geometry
attr_accessor :height, :width
def initialize width = nil, height = nil
@height = (height || width).to_f
@width = (width || height).to_f
end
def self.parse string
if match = (string && string.match(/(\d*)x(\d*)/))
Geometry.new(*match[1,2])
end
end
def square?
height == width
end
def horizontal?
height < width
end
def vertical?
height > width
end
def aspect
width / height
end
def larger
[height, width].max
end
def smaller
[height, width].min
end
def to_s
"#{width}x#{height}"
end
attr_accessor :file, :current_geometry, :target_geometry, :format, :whiny_thumbnails
# Creates a Thumbnail object set to work on the +file+ given. It
# will attempt to transform the image into one defined by +target_geometry+
# which is a "WxH"-style string. +format+ will be inferred from the +file+
# unless specified. Thumbnail creation will raise no errors unless
# +whiny_thumbnails+ is true (which it is, by default.
def initialize file, target_geometry, format = nil, whiny_thumbnails = true
@file = file
@crop = target_geometry[-1,1] == '#'
@target_geometry = Geometry.parse target_geometry
@current_geometry = Geometry.from_file file
@whiny_thumbnails = whiny_thumbnails
@current_format = File.extname(@file.path)
@basename = File.basename(@file.path, @current_format)
def inspect
to_s
end
@format = format
end
attr_accessor :geometry, :data
def initialize geometry, data, whiny_thumbnails = nil
@geometry, @data = geometry, data
@whiny_thumbnails = Paperclip.options[:whiny_thumbnails]
@whiny_thumbnails = whiny_thumbnails unless whiny_thumbnails.nil?
# Creates a thumbnail, as specified in +initialize+, +make+s it, and returns the
# resulting Tempfile.
def self.make file, dimensions, format = nil, whiny_thumbnails = true
new(file, dimensions, format, whiny_thumbnails).make
end
def self.make geometry, data, whiny_thumbnails = nil
new(geometry, data, whiny_thumbnails).make
# Returns true if the +target_geometry+ is meant to crop.
def crop?
@crop
end
# Performs the conversion of the +file+ into a thumbnail. Returns the Tempfile
# that contains the new image.
def make
return data if geometry.nil?
operator = geometry[-1,1]
begin
scale_geometry = geometry
scale_geometry, crop_geometry = geometry_for_crop if operator == '#'
convert = Paperclip.path_for_command("convert")
command = "#{convert} - -scale '#{scale_geometry}' #{operator == '#' ? "-crop '#{crop_geometry}'" : ""} - 2>/dev/null"
thumb = piping data, :to => command
rescue Errno::EPIPE => e
raise PaperclipError, "could not be thumbnailed. Is ImageMagick or GraphicsMagick installed and available?"
rescue SystemCallError => e
raise PaperclipError, "could not be thumbnailed."
rescue PaperclipError
raise if @whiny_thumbnails
end
if @whiny_thumbnails && !$?.success?
raise PaperclipError, "could not be thumbnailed because of an error with 'convert'."
end
thumb
end
src = @file
dst = Tempfile.new([@basename, @format].compact.join("."))
command = <<-end_command
#{ Paperclip.path_for_command('convert') }
"#{ File.expand_path(src.path) }"
#{ transformation_command }
"#{ File.expand_path(dst.path) }"
end_command
success = system(command.gsub(/\s+/, " "))
def geometry_for_crop
identify = Paperclip.path_for_command("identify")
piping data, :to => "#{identify} - 2>/dev/null" do |pipeout|
dimensions = pipeout.split[2]
if src = Geometry.parse(dimensions)
dst = Geometry.parse(geometry)
ratio = Geometry.new( dst.width / src.width, dst.height / src.height )
scale_geometry, scale = if ratio.horizontal? || ratio.square?
[ "%dx" % dst.width, ratio.width ]
else
[ "x%d" % dst.height, ratio.height ]
end
crop_geometry = if ratio.horizontal? || ratio.square?
"%dx%d+%d+%d" % [ dst.width, dst.height, 0, (src.height * scale - dst.height) / 2 ]
else
"%dx%d+%d+%d" % [ dst.width, dst.height, (src.width * scale - dst.width) / 2, 0 ]
end
[ scale_geometry, crop_geometry ]
else
raise PaperclipError, "does not contain a valid image."
end
if success && $?.exitstatus != 0 && @whiny_thumbnails
raise PaperclipError, "There was an error processing this thumbnail"
end
dst
end
def piping data, command, &block
self.class.piping(data, command, &block)
# Returns the command ImageMagick's +convert+ needs to transform the image
# into the thumbnail.
def transformation_command
scale, crop = @current_geometry.transformation_to(@target_geometry, crop?)
trans = "-scale #{scale}"
trans << " -crop #{crop} +repage" if crop
trans
end
end
def self.piping data, command, &block
command = command[:to] if command.respond_to?(:[]) && command[:to]
block ||= lambda {|d| d }
IO.popen(command, "w+") do |io|
io.write(data)
io.close_write
block.call(io.read)
end
# Due to how ImageMagick handles its image format conversion and how Tempfile
# handles its naming scheme, it is necessary to override how Tempfile makes
# its names so as to allow for file extensions. Idea taken from the comments
# on this blog post:
# http://marsorange.com/archives/of-mogrify-ruby-tempfile-dynamic-class-definitions
class Tempfile < ::Tempfile
# Replaces Tempfile's +make_tmpname+ with one that honors file extensions.
def make_tmpname(basename, n)
extension = File.extname(basename)
sprintf("%s,%d,%d%s", File.basename(basename, extension), $$, n, extension)
end
end
end
......@@ -26,3 +26,7 @@ module Paperclip
end
end
class File #:nodoc:
include Paperclip::Upfile
end
......@@ -2,7 +2,7 @@ require 'environment'
def obtain_class
class_name = ENV['CLASS'] || ENV['class']
@klass = eval(class_name)
@klass = Object.const_get(class_name)
end
def obtain_attachments
......@@ -10,7 +10,7 @@ def obtain_attachments
if !name.blank? && @klass.attachment_names.include?(name)
[ name ]
else
@klass.attachment_names
@klass.attachment_definitions.keys
end
end
......@@ -21,13 +21,18 @@ namespace :paperclip do
instances = klass.find(:all)
names = obtain_attachments
puts "Regenerating thumbnails for #{instances.length} instances:"
puts "Regenerating thumbnails for #{instances.length} instances of #{klass.name}:"
instances.each do |instance|
names.each do |name|
instance.send("process_#{name}_thumbnails")
result = if instance.send("#{ name }?")
instance.send(name).send("post_process")
instance.send(name).save
else
true
end
print result ? "." : "x"; $stdout.flush
end
print instance.save ? "." : "x"; $stdout.flush
end
puts " Done."
end
end
\ No newline at end of file
end
test:
adapter: sqlite3
#dbfile: paperclip.db
database: ":memory:"
\ No newline at end of file
database: ":memory:"
This is not an image.
require 'rubygems'
require 'test/unit'
require 'shoulda'
require 'mocha'
require 'tempfile'
require 'active_record'
require 'ruby-debug'
ROOT = File.join(File.dirname(__FILE__), '..')
RAILS_ROOT = ROOT
$LOAD_PATH << File.join(ROOT, 'lib')
$LOAD_PATH << File.join(ROOT, 'lib', 'paperclip')
require File.join(ROOT, 'lib', 'paperclip.rb')
FIXTURES_DIR = File.join(File.dirname(__FILE__), "fixtures")
config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
ActiveRecord::Base.establish_connection(config[ENV['RAILS_ENV'] || 'test'])
def rebuild_model options = {}
ActiveRecord::Base.connection.create_table :dummies, :force => true do |table|
table.column :avatar_file_name, :string
table.column :avatar_content_type, :string
table.column :avatar_file_size, :integer
end
ActiveRecord::Base.send(:include, Paperclip)
Object.send(:remove_const, "Dummy") rescue nil
Object.const_set("Dummy", Class.new(ActiveRecord::Base))
Dummy.class_eval do
include Paperclip
has_attached_file :avatar, options
end
end
class << Test::Unit::TestCase
def context name, &block
(@contexts ||= []) << name
(@context_blocks ||= []) << block
saved_setups = (@context_setups ||= []).dup
saved_teardowns = (@context_teardowns ||= []).dup
self.instance_eval(&block)
@context_setups = saved_setups
@context_teardowns = saved_teardowns
@contexts.pop
@context_blocks.pop
end
def setup &block
@context_setups << block
end
def teardown &block
@context_teardowns << block
end
def should name, &test
context_setups = @context_setups.dup
context_teardowns = @context_teardowns.dup
define_method(["test:", @contexts, "should", name].flatten.join(" ")) do
context_setups.each { |setup| self.instance_eval(&setup) }
self.instance_eval(&test)
context_teardowns.each { |teardown| self.instance_eval(&teardown) }
end
end
def should_eventually name
define_method(["test:", @contexts, "should eventually", name].flatten.join(" ")) do
STDOUT.print "X"
assert true
end
end
end
\ No newline at end of file
require 'test/unit'
require File.dirname(__FILE__) + "/test_helper.rb"
require 'test/helper'
class TestAttachment < Test::Unit::TestCase
class Dummy
# This is a dummy class
end
class AttachmentTest < Test::Unit::TestCase
context "An attachment" do
setup do
@dummy = {}
@definition = Paperclip::AttachmentDefinition.new("thing", {})
@attachment = Paperclip::Attachment.new(@dummy, "thing", @definition)
@default_options = {
:path => ":rails_root/tmp/:attachment/:class/:style/:id/:basename.:extension"
}
@instance = stub
@instance.stubs(:id).returns(41)
@instance.stubs(:class).returns(Dummy)
@instance.stubs(:[]).with(:test_file_name).returns("5k.png")
@instance.stubs(:[]).with(:test_content_type).returns("image/png")
@instance.stubs(:[]).with(:test_file_size).returns(12345)
@attachment = Paperclip::Attachment.new(:test, @instance, @default_options)
@file = File.new(File.join(File.dirname(__FILE__), "fixtures", "5k.png"))
end
end
context "The class Foo" do
setup do
ActiveRecord::Base.connection.create_table :foos, :force => true do |table|
table.column :image_file_name, :string
table.column :image_content_type, :string
table.column :image_file_size, :integer
table.column :document_file_name, :string
table.column :document_content_type, :string
table.column :document_file_size, :integer
end
Object.send(:remove_const, :Foo) rescue nil
class ::Foo < ActiveRecord::Base; end
end
context "with an image attached to :image" do
context "when expecting three styles" do
setup do
assert Foo.has_attached_file(:image)
@foo = Foo.new
@file = File.new(File.join(File.dirname(__FILE__), "fixtures", "test_image.jpg"))
assert_nothing_raised{ @foo.image = @file }
end
should "be able to have a file assigned with :image=" do
assert_equal "test_image.jpg", @foo.image.original_filename
assert_equal "image/jpg", @foo.image.content_type
end
should "be able to retrieve the data as a blob" do
@file.rewind
assert_equal @file.read, @foo.image.read
@attachment = Paperclip::Attachment.new(:test, @instance, @default_options.merge({
:styles => { :large => ["400x400", :png],
:medium => ["100x100", :gif],
:small => ["32x32#", :jpg]}
}))
end
context "and saved" do
context "and assigned a file" do
setup do
assert @foo.save
@instance.expects(:[]=).with(:test_file_name, File.basename(@file.path))
@instance.expects(:[]=).with(:test_content_type, "image/png")
@instance.expects(:[]=).with(:test_file_size, @file.size)
@instance.expects(:[]=).with(:test_file_name, nil)
@instance.expects(:[]=).with(:test_content_type, nil)
@instance.expects(:[]=).with(:test_file_size, nil)
@attachment.assign(@file)
end
should "have no errors" do
assert @foo.image.errors.blank?
assert @foo.errors.blank?
should "be dirty" do
assert @attachment.dirty?
end
should "have a file on the filesystem" do
assert @foo.image.send(:file_name)
assert File.file?(@foo.image.send(:file_name)), @foo.image.send(:file_name)
assert File.size(@foo.image.send(:file_name)) > 0
assert_match /405x375/, `identify '#{@foo.image.send(:file_name)}'`
assert_equal IO.read(@file.path), @foo.image.read
end
context "and then deleted" do
setup do
assert @foo.destroy
end
should "have no errors" do
assert @foo.image.errors.blank?
end
should "have no files on the filesystem" do
assert !File.file?(@foo.image.send(:file_name)), @foo.image.send(:file_name)
assert !File.exist?(@foo.image.send(:file_name)), @foo.image.send(:file_name)
should "have its image and attachments as tempfiles" do
[nil, :large, :medium, :small].each do |style|
assert File.exists?(@attachment.to_io(style))
end
end
context "and then set to null and resaved" do
context "and saved" do
setup do
@foo.image = nil
assert @foo.save
@attachment.save
end
should "have no errors" do
assert @foo.image.errors.blank?
should "commit the files to disk" do
[nil, :large, :medium, :small].each do |style|
io = @attachment.to_io(style)
assert File.exists?(io)
assert ! io.is_a?(::Tempfile)
end
end
should "have no files on the filesystem" do
assert !File.file?(@foo.image.send(:file_name)), @foo.image.send(:file_name)
assert !File.exist?(@foo.image.send(:file_name)), @foo.image.send(:file_name)
should "save the files as the right formats and sizes" do
[[:large, 400, 61, "PNG"], [:medium, 100, 15, "GIF"], [:small, 32, 32, "JPEG"]].each do |style|
out = `identify -format "%w %h %b %m" #{@attachment.to_io(style.first).path}`
width, height, size, format = out.split(" ")
assert_equal style[1].to_s, width.to_s
assert_equal style[2].to_s, height.to_s
assert_equal style[3].to_s, format.to_s
end
end
end
end
end
context "with an image with thumbnails attached to :image and saved" do
setup do
assert Foo.has_attached_file(:image, :styles => {:small => "16x16", :medium => "100x100", :large => "250x250", :square => "32x32#"})
@foo = Foo.new
@file = File.new(File.join(File.dirname(__FILE__), "fixtures", "test_image.jpg"))
assert_nothing_raised{ @foo.image = @file }
assert @foo.save
end
should "have no errors" do
assert @foo.image.errors.blank?, @foo.image.errors.inspect
assert @foo.errors.blank?
end
[:original, :small, :medium, :large, :square].each do |style|
should "have a file for #{style} on the filesystem" do
assert @foo.image.send(:file_name)
assert File.file?(@foo.image.send(:file_name)), @foo.image.send(:file_name)
assert File.size(@foo.image.send(:file_name)) > 0
assert_equal IO.read(@file.path), @foo.image.read
end
should "return the correct urls when asked for the #{style} image" do
assert_equal "/foos/images/1/#{style}_test_image.jpg", @foo.image.url(style)
end
end
should "produce the correct dimensions when each style is identified" do
assert_match /16x15/, `identify '#{@foo.image.send(:file_name, :small)}'`
assert_match /32x32/, `identify '#{@foo.image.send(:file_name, :square)}'`
assert_match /100x93/, `identify '#{@foo.image.send(:file_name, :medium)}'`
assert_match /250x231/, `identify '#{@foo.image.send(:file_name, :large)}'`
assert_match /405x375/, `identify '#{@foo.image.send(:file_name, :original)}'`
end
end
context "with an image with thumbnails attached to :image and a document attached to :document" do
end
context "with an invalid image with a square thumbnail attached to :image" do
setup do
assert Foo.has_attached_file(:image, :styles => {:square => "32x32#"})
assert Foo.validates_attached_file(:image)
@foo = Foo.new
@file = File.new(File.join(File.dirname(__FILE__), "fixtures", "test_invalid_image.jpg"))
assert_nothing_raised{ @foo.image = @file }
end
should "not save and should report errors from identify" do
assert !@foo.save
assert @foo.errors.on(:image)
assert @foo.errors.on(:image).any?{|e| e.match(/does not contain a valid image/) }, @foo.errors.on(:image)
end
end
context "with an invalid image attached to :image" do
setup do
assert Foo.has_attached_file(:image, :styles => {:sorta_square => "32x32"})
assert Foo.validates_attached_file(:image)
@foo = Foo.new
@file = File.new(File.join(File.dirname(__FILE__), "fixtures", "test_invalid_image.jpg"))
assert_nothing_raised{ @foo.image = @file }
end
should "not save and should report errors from convert" do
assert !@foo.save
assert @foo.errors.on(:image)
assert @foo.errors.on(:image).any?{|e| e.match(/because of an error/) }, @foo.errors.on(:image)
end
end
end
end
\ No newline at end of file
end
end
require 'test/unit'
require File.dirname(__FILE__) + "/test_helper.rb"
class TestAttachmentDefinition < Test::Unit::TestCase
context "Attachment definitions" do
should "allow overriding options" do
not_expected = Paperclip::AttachmentDefinition.defaults[:path]
Paperclip::AttachmentDefinition.defaults[:path] = "123"
assert_not_equal not_expected, Paperclip::AttachmentDefinition.defaults[:path]
assert_equal "123", Paperclip::AttachmentDefinition.defaults[:path]
end
should "accept options that override defaults" do
@def = Paperclip::AttachmentDefinition.new "attachment", :path => "123", :delete_on_destroy => false
assert_not_equal Paperclip::AttachmentDefinition.defaults[:path], @def.path
assert_not_equal Paperclip::AttachmentDefinition.defaults[:delete_on_destroy], @def.delete_on_destroy
assert_equal "123", @def.path
assert_equal false, @def.delete_on_destroy
end
end
context "An attachment defintion" do
setup do
@options = {
:path => "/home/stuff/place",
:url => "/attachments/:attachment/:name",
:custom_definition => :boogie!,
:styles => {:thumb => "100x100", :large => "300x300>"},
:validates_existance => true,
:validates_size => [0, 2048]
}
@def = Paperclip::AttachmentDefinition.new "attachment", @options
end
should "automatically look in the hash for missing methods" do
assert ! @def.respond_to?(:custom_defintion)
assert_equal :boogie!, @def.custom_definition
end
should "be able to read options using attribute readers" do
(@options.keys - [:styles]).each do |key|
assert_equal @options[key], @def.send(key)
end
end
should "return styles as the styles option plus the original" do
assert_equal( (@options[:styles].keys + [:original]).map(&:to_s).sort.uniq, @def.styles.keys.map(&:to_s).sort )
end
should "return all validations when sent :validations" do
assert @def.validations[:existance] == true, @def.validations[:existance]
assert @def.validations[:size] == [0, 2048], @def.validations[:size]
end
end
end
\ No newline at end of file
require 'rubygems'
require 'test/unit'
require 'shoulda'
require File.join(File.dirname(__FILE__), '..', 'lib', 'paperclip', 'geometry.rb')
class PaperclipTest < Test::Unit::TestCase
context "Paperclip::Geometry" do
should "correctly report its given dimensions" do
assert @geo = Paperclip::Geometry.new(1024, 768)
assert_equal 1024, @geo.width
assert_equal 768, @geo.height
end
should "correctly create a square if the height dimension is missing" do
assert @geo = Paperclip::Geometry.new(1024)
assert_equal 1024, @geo.width
assert_equal 1024, @geo.height
end
should "correctly create a square if the width dimension is missing" do
assert @geo = Paperclip::Geometry.new(nil, 768)
assert_equal 768, @geo.width
assert_equal 768, @geo.height
end
should "be generated from a WxH-formatted string" do
assert @geo = Paperclip::Geometry.parse("800x600")
assert_equal 800, @geo.width
assert_equal 600, @geo.height
end
should "be generated from a xH-formatted string" do
assert @geo = Paperclip::Geometry.parse("x600")
assert_equal 600, @geo.width
assert_equal 600, @geo.height
end
should "be generated from a Wx-formatted string" do
assert @geo = Paperclip::Geometry.parse("800x")
assert_equal 800, @geo.width
assert_equal 800, @geo.height
end
should "be generated from a file" do
file = Dir.glob("/Users/jyurek/Pictures/*.jpg").first
file = File.new(file)
assert_nothing_raised{ @geo = Paperclip::Geometry.from_file(file) }
assert @geo.height > 0
assert @geo.width > 0
end
should "be generated from a file path" do
file = Dir.glob("/Users/jyurek/Pictures/*.jpg").first
assert_nothing_raised{ @geo = Paperclip::Geometry.from_file(file) }
assert @geo.height > 0
assert @geo.width > 0
end
should "not generate from a bad file" do
file = "/home/This File Does Not Exist.omg"
assert_raise(Errno::ENOENT){ @geo = Paperclip::Geometry.from_file(file) }
end
[['vertical', 900, 1440, true, false, false, 1440, 900, 0.625],
['horizontal', 1024, 768, false, true, false, 1024, 768, 1.3333],
['square', 100, 100, false, false, true, 100, 100, 1]].each do |args|
context "performing calculations on a #{args[0]} viewport" do
setup do
@geo = Paperclip::Geometry.new(args[1], args[2])
end
should "#{args[3] ? "" : "not"} be vertical" do
assert_equal args[3], @geo.vertical?
end
should "#{args[4] ? "" : "not"} be horizontal" do
assert_equal args[4], @geo.horizontal?
end
should "#{args[5] ? "" : "not"} be square" do
assert_equal args[5], @geo.square?
end
should "report that #{args[6]} is the larger dimension" do
assert_equal args[6], @geo.larger
end
should "report that #{args[7]} is the smaller dimension" do
assert_equal args[7], @geo.smaller
end
should "have an aspect ratio of #{args[8]}" do
assert_in_delta args[8], @geo.aspect, 0.0001
end
end
end
[[ [1000, 100], [64, 64], "x64", "64x64+288+0" ],
[ [100, 1000], [50, 950], "x950", "50x950+22+0" ],
[ [100, 1000], [50, 25], "50x", "50x25+0+237" ]]. each do |args|
context "of #{args[0].inspect} and given a Geometry #{args[1].inspect} and sent transform_to" do
setup do
@geo = Paperclip::Geometry.new(*args[0])
@dst = Paperclip::Geometry.new(*args[1])
@scale, @crop = @geo.transformation_to @dst, true
end
should "be able to return the correct scaling transformation geometry #{args[2]}" do
assert_equal args[2], @scale
end
should "be able to return the correct crop transformation geometry #{args[3]}" do
assert_equal args[3], @crop
end
end
end
end
end
require 'rubygems'
require 'test/unit'
require 'active_record'
require 'active_record/fixtures'
require 'fileutils'
require 'pp'
require File.dirname(__FILE__) + "/simply_shoulda.rb"
require File.dirname(__FILE__) + "/../init.rb"
config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
ActiveRecord::Base.establish_connection(config[ENV['RAILS_ENV'] || 'test'])
Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures/"
$LOAD_PATH.unshift(Test::Unit::TestCase.fixture_path)
RAILS_ROOT = "test" unless Object.const_defined? "RAILS_ROOT"
class Test::Unit::TestCase #:nodoc:
def create_fixtures(*table_names)
if block_given?
Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names) { yield }
else
Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names)
end
end
def self.load_all_fixtures
all_fixtures = Dir.glob("#{File.dirname(__FILE__)}/fixtures/*.yml").collect do |f|
puts "Loading fixture '#{f}'"
File.basename(f).gsub(/\.yml$/, "").to_sym
end
Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, all_fixtures)
end
# Turn off transactional fixtures if you're working with MyISAM tables in MySQL
self.use_transactional_fixtures = true
# Instantiated fixtures are slow, but give you @david where you otherwise would need people(:david)
self.use_instantiated_fixtures = false
# Add more helper methods to be used by all tests here...
end
\ No newline at end of file
require 'test/helper.rb'
class PaperclipTest < Test::Unit::TestCase
context "A model with an attachment" do
setup do
rebuild_model :styles => { :large => "300x300>",
:medium => "100x100",
:thumb => ["32x32#", :gif] },
:default_style => :medium,
:url => "/:attachment/:class/:style/:id/:basename.:extension",
:path => ":rails_root/tmp/:attachment/:class/:style/:id/:basename.:extension"
end
should "integrate" do
@dummy = Dummy.new
@file = File.new(File.join(FIXTURES_DIR, "5k.png"))
@bad_file = File.new(File.join(FIXTURES_DIR, "bad.png"))
assert @dummy.avatar = @file
assert @dummy.valid?
assert @dummy.save
assert_equal "100x15", `identify -format "%wx%h" #{@dummy.avatar.to_io.path}`.chomp
assert_equal "434x66", `identify -format "%wx%h" #{@dummy.avatar.to_io(:original).path}`.chomp
assert_equal "300x46", `identify -format "%wx%h" #{@dummy.avatar.to_io(:large).path}`.chomp
assert_equal "100x15", `identify -format "%wx%h" #{@dummy.avatar.to_io(:medium).path}`.chomp
assert_equal "32x32", `identify -format "%wx%h" #{@dummy.avatar.to_io(:thumb).path}`.chomp
saved_paths = [:thumb, :medium, :large, :original].collect{|s| @dummy.avatar.to_io(s).path }
@d2 = Dummy.find(@dummy.id)
assert_equal "100x15", `identify -format "%wx%h" #{@dummy.avatar.to_io.path}`.chomp
assert_equal "434x66", `identify -format "%wx%h" #{@dummy.avatar.to_io(:original).path}`.chomp
assert_equal "300x46", `identify -format "%wx%h" #{@d2.avatar.to_io(:large).path}`.chomp
assert_equal "100x15", `identify -format "%wx%h" #{@d2.avatar.to_io(:medium).path}`.chomp
assert_equal "32x32", `identify -format "%wx%h" #{@d2.avatar.to_io(:thumb).path}`.chomp
@dummy.avatar = nil
assert_nil @dummy.avatar_file_name
assert @dummy.valid?
assert @dummy.save
saved_paths.each do |p|
assert ! File.exists?(p)
end
@d2 = Dummy.find(@dummy.id)
assert_nil @d2.avatar_file_name
@d2.avatar = @bad_file
assert ! @d2.valid?
@d2.avatar = nil
assert @d2.valid?
Dummy.validates_attachment_presence :avatar
@d3 = Dummy.find(@d2.id)
@d3.avatar = @file
assert @d3.valid?
@d3.avatar = @bad_file
assert ! @d3.valid?
@d3.avatar = nil
assert ! @d3.valid?
@dummy.avatar = @file
assert @dummy.save
@dummy.avatar = nil
assert_nil @dummy.avatar_file_name
@dummy.reload
assert_equal "5k.png", @dummy.avatar_file_name
end
end
end
require 'rubygems'
require 'test/unit'
require 'stringio'
require 'tempfile'
require 'shoulda'
require File.join(File.dirname(__FILE__), '..', 'lib', 'paperclip', 'iostream.rb')
class IOStreamTest < Test::Unit::TestCase
context "IOStream" do
should "be included in IO, File, Tempfile, and StringIO" do
[IO, File, Tempfile, StringIO].each do |klass|
assert klass.included_modules.include?(IOStream), "Not in #{klass}"
end
end
end
context "A file" do
setup do
@file = File.new(File.join(File.dirname(__FILE__), "fixtures", "5k.png"))
end
context "that is sent #stream_to" do
[["/tmp/iostream.string.test", File],
[Tempfile.new('iostream.test'), Tempfile]].each do |args|
context "and given a #{args[0].class.to_s}" do
setup do
assert @result = @file.stream_to(args[0])
end
should "return a #{args[1].to_s}" do
assert @result.is_a?(args[1])
end
should "contain the same data as the original file" do
@file.rewind; @result.rewind
assert_equal @file.read, @result.read
end
end
end
end
context "that is sent #to_tempfile" do
setup do
assert @tempfile = @file.to_tempfile
end
should "convert it to a Tempfile" do
assert @tempfile.is_a?(Tempfile)
end
should "have the Tempfile contain the same data as the file" do
@file.rewind; @tempfile.rewind
assert_equal @file.read, @tempfile.read
end
end
end
end
require 'test/unit'
require File.dirname(__FILE__) + "/test_helper.rb"
require 'test/helper.rb'
class TestPaperclip < Test::Unit::TestCase
context "Paperclip" do
should "allow overriding options" do
[:image_magick_path, :whiny_deletes, :whiny_thumbnails].each do |option|
not_expected = Paperclip.options[option]
Paperclip.options[option] = "123"
assert_equal "123", Paperclip.options[option]
assert_not_equal not_expected, Paperclip.options[option]
end
class PaperclipTest < Test::Unit::TestCase
context "An ActiveRecord model with an 'avatar' attachment" do
setup do
rebuild_model
end
should "give the correct path for a command" do
expected = "/usr/bin/wtf"
Paperclip.options[:image_magick_path] = "/usr/bin"
assert_equal expected, Paperclip.path_for_command("wtf")
should "have an #avatar method" do
assert Dummy.new.respond_to?(:avatar)
end
expected = "wtf"
Paperclip.options[:image_magick_path] = nil
assert_equal expected, Paperclip.path_for_command("wtf")
should "have an #avatar= method" do
assert Dummy.new.respond_to?(:avatar=)
end
context "being used on improper class Improper" do
setup do
ActiveRecord::Base.connection.create_table :impropers, :force => true do |table|
# Empty table
[[:presence, nil, "5k.png", nil],
[:size, {:in => 1..10240}, "5k.png", "12k.png"]].each do |args|
context "with #{args[0]} validations" do
setup do
Dummy.class_eval do
send(*[:"validates_attachment_#{args[0]}", :avatar, args[1]].compact)
end
@dummy = Dummy.new
end
Object.send(:remove_const, :Improper) rescue nil
class ::Improper < ActiveRecord::Base; end
end
should "raises an error when an attachment is defined" do
assert_raises(Paperclip::PaperclipError){ Improper.has_attached_file :image }
end
[:file_name, :content_type].each do |column|
context "which has only the #{column} column" do
context "and a valid file" do
setup do
ActiveRecord::Base.connection.create_table :impropers, :force => true do |table|
table.column :"image_#{column}", :string
end
Object.send(:remove_const, :Improper) rescue nil
class ::Improper < ActiveRecord::Base; end
@file = args[2] && File.new(File.join(FIXTURES_DIR, args[2]))
end
should "raises an error when an attachment is defined" do
assert_raises(Paperclip::PaperclipError){ Improper.has_attached_file :image }
should "not have any errors" do
@dummy.avatar = @file
assert @dummy.avatar.valid?
assert_equal 0, @dummy.avatar.errors.length
end
end
end
end
context "being used on class Foo" do
setup do
ActiveRecord::Base.connection.create_table :foos, :force => true do |table|
table.column :image_file_name, :string
table.column :image_content_type, :string
table.column :image_file_size, :integer
context "and an invalid file" do
setup do
@file = args[3] && File.new(File.join(FIXTURES_DIR, args[3]))
end
table.column :document_file_name, :string
table.column :document_content_type, :string
table.column :document_file_size, :integer
should "have errors" do
@dummy.avatar = @file
assert ! @dummy.avatar.valid?
assert_equal 1, @dummy.avatar.errors.length
end
end
Object.send(:remove_const, :Foo) rescue nil
class ::Foo < ActiveRecord::Base; end
end
should "be able to assign a default attachment" do
assert Foo.has_attached_file(:image)
assert_equal [:image], Foo.attached_files
foo = Foo.new
assert foo.respond_to?(:image)
assert foo.image.is_a?(Paperclip::Attachment)
end
should "be able to assign two attachments separately" do
assert Foo.has_attached_file(:image)
assert Foo.has_attached_file(:document)
assert_equal [:image, :document], Foo.attached_files
foo = Foo.new
assert foo.respond_to?(:image)
assert foo.respond_to?(:document)
assert foo.image.is_a?(Paperclip::Attachment)
assert foo.document.is_a?(Paperclip::Attachment)
assert foo.image != foo.document
end
should "be able to assign two attachments simultaneously" do
assert Foo.has_attached_file(:image, :document)
assert_equal [:image, :document], Foo.attached_files
foo = Foo.new
assert foo.respond_to?(:image)
assert foo.respond_to?(:document)
assert foo.image.is_a?(Paperclip::Attachment)
assert foo.document.is_a?(Paperclip::Attachment)
assert foo.image != foo.document
end
should "be able to set options on attachments" do
assert Foo.has_attached_file :image, :thumbnails => {:thumb => "100x100"}
assert_equal [:image], Foo.attached_files
assert_equal( {:thumb => "100x100"}, Foo.attachment_definition_for(:image).thumbnails )
foo = Foo.new
assert foo.respond_to?(:image)
assert foo.image.is_a?(Paperclip::Attachment)
end
end
end
end
\ No newline at end of file
end
require 'rubygems'
require 'test/unit'
require 'shoulda'
require 'mocha'
require 'tempfile'
require File.join(File.dirname(__FILE__), '..', 'lib', 'paperclip', 'geometry.rb')
require File.join(File.dirname(__FILE__), '..', 'lib', 'paperclip', 'thumbnail.rb')
class ThumbnailTest < Test::Unit::TestCase
context "A Paperclip Tempfile" do
setup do
@tempfile = Paperclip::Tempfile.new("file.jpg")
end
should "have its path contain a real extension" do
assert_equal ".jpg", File.extname(@tempfile.path)
end
should "be a real Tempfile" do
assert @tempfile.is_a?(::Tempfile)
end
end
context "Another Paperclip Tempfile" do
setup do
@tempfile = Paperclip::Tempfile.new("file")
end
should "not have an extension if not given one" do
assert_equal "", File.extname(@tempfile.path)
end
should "still be a real Tempfile" do
assert @tempfile.is_a?(::Tempfile)
end
end
context "An image" do
setup do
@file = File.new(File.join(File.dirname(__FILE__), "fixtures", "5k.png"))
end
context "being thumbnailed at 100x50 with cropping" do
setup do
@thumb = Paperclip::Thumbnail.new(@file, "100x50#")
end
should "report its correct current and target geometries" do
assert_equal "100x50", @thumb.target_geometry.to_s
assert_equal "434x66", @thumb.current_geometry.to_s
end
should "report its correct format" do
assert_nil @thumb.format
end
should "have whiny_thumbnails turned on by default" do
assert @thumb.whiny_thumbnails
end
should "send the right command to convert when sent #make" do
@thumb.expects(:system).with do |arg|
arg.match %r{convert\s+"#{File.expand_path(@thumb.file.path)}"\s+-scale\s+x50\s+-crop\s+100x50\+114\+0\s+\+repage\s+".*?"}
end
@thumb.make
end
should "create the thumbnail when sent #make" do
dst = @thumb.make
assert_match /100x50/, `identify #{dst.path}`
end
end
end
end
require 'test/unit'
require File.dirname(__FILE__) + "/test_helper.rb"
class TestThumbnailer < Test::Unit::TestCase
context "The Thumbnailer" do
should "calculate geometries for cropping images" do
@file = IO.read(File.join(File.dirname(__FILE__), "fixtures", "test_image.jpg"))
assert_equal ["50x", "50x25+0+10"], Paperclip::Thumbnail.new("50x25", @file).geometry_for_crop
assert_equal ["x50", "50x50+2+0"], Paperclip::Thumbnail.new("50x50", @file).geometry_for_crop
assert_equal ["x50", "25x50+14+0"], Paperclip::Thumbnail.new("25x50", @file).geometry_for_crop
end
should "be able to pipe commands" do
doc = %w(one two three four five)
expected = %w(five four one three two)
assert_equal expected, Paperclip::Thumbnail.piping(doc.join("\n"), "sort").split("\n")
assert_equal "Hello, World!\n", Paperclip::Thumbnail.piping("World", "ruby -e 'puts %Q{Hello, \#{STDIN.read}!}'")
end
[
[:square, 125, 125],
[:long, 53, 225],
[:tall, 225, 53],
[:tiny, 16, 16]
].each do |size, width, height|
context "given a #{size} image" do
setup do
@base_file = IO.read(File.join(File.dirname(__FILE__), "fixtures", "test_image.jpg"))
@file = Paperclip::Thumbnail.piping(@base_file, "convert - -scale '#{width}x#{height}!' -")
assert_match /#{width}x#{height}/, Paperclip::Thumbnailer.piping(@file, "identify -")
@targets = {
:small => "50x50",
:same => nil,
:large => "100x100",
:shrink_to_large => "100x100>",
:crop_medium => "75x75#"
}
end
should_eventually "generate correct thumbnails for the image"
end
end
end
end
\ No newline at end of file
require 'test/unit'
require File.dirname(__FILE__) + "/test_helper.rb"
class TestUpfile < Test::Unit::TestCase
context "Using Upfile" do
setup do
File.send :include, Paperclip::Upfile
@filename = File.join(File.dirname(__FILE__), "fixtures", "test_image.jpg")
@file = File.new(@filename)
end
should "allow File objects to respond as uploaded files do" do
assert_respond_to @file, :original_filename
assert_respond_to @file, :content_type
assert_respond_to @file, :size
assert_equal "test_image.jpg", @file.original_filename
assert_equal "image/jpg", @file.content_type
assert_equal @file.stat.size, @file.size
end
end
end
\ No newline at end of file
# Uninstall hook code here
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment