Commit bc5c51d1 by Mike Burns Committed by Mike Burns

Separate the UrlGenerator out from the Attachment. Some example plugins that…

Separate the UrlGenerator out from the Attachment. Some example plugins that could be written include generating thumbnails on the fly for different thumbnail sizes, or delaying the thumbnail generation until it is first called.
parent af689b44
# encoding: utf-8
require 'uri'
require 'paperclip/url_generator'
module Paperclip
# The Attachment class manages the files for a given attachment. It saves
......@@ -25,7 +26,9 @@ module Paperclip
:use_default_time_zone => true,
:hash_digest => "SHA1",
:hash_data => ":class/:attachment/:id/:style/:updated_at",
:preserve_files => false
:preserve_files => false,
:interpolator => Paperclip::Interpolations,
:url_generator => Paperclip::UrlGenerator
}
end
......@@ -38,25 +41,26 @@ module Paperclip
#
# Options include:
#
# +url+ - a relative URL of the attachment. This is interpolated using +interpolator+
# +path+ - where on the filesystem to store the attachment. This is interpolated using +interpolator+
# +styles+ - a hash of options for processing the attachment. See +has_attached_file+ for the details
# +only_process+ - style args to be run through the post-processor. This defaults to the empty list
# +default_url+ - a URL for the missing image
# +default_style+ - the style to use when don't specify an argument to e.g. #url, #path
# +storage+ - the storage mechanism. Defaults to :filesystem
# +use_timestamp+ - whether to append an anti-caching timestamp to image URLs. Defaults to true
# +whiny+, +whiny_thumbnails+ - whether to raise when thumbnailing fails
# +use_default_time_zone+ - related to +use_timestamp+. Defaults to true
# +hash_digest+ - a string representing a class that will be used to hash URLs for obfuscation
# +hash_data+ - the relative URL for the hash data. This is interpolated using +interpolator+
# +hash_secret+ - a secret passed to the +hash_digest+
# +convert_options+ - flags passed to the +convert+ command for processing
# +source_file_options+ - flags passed to the +convert+ command that controls how the file is read
# +processors+ - classes that transform the attachment. Defaults to [:thumbnail]
# +preserve_files+ - whether to keep files on the filesystem when deleting to clearing the attachment. Defaults to false
# +interpolator+ - the object used to interpolate filenames and URLs. Defaults to Paperclip::Interpolations
def initialize name, instance, options = {}
# +url+ - a relative URL of the attachment. This is interpolated using +interpolator+
# +path+ - where on the filesystem to store the attachment. This is interpolated using +interpolator+
# +styles+ - a hash of options for processing the attachment. See +has_attached_file+ for the details
# +only_process+ - style args to be run through the post-processor. This defaults to the empty list
# +default_url+ - a URL for the missing image
# +default_style+ - the style to use when don't specify an argument to e.g. #url, #path
# +storage+ - the storage mechanism. Defaults to :filesystem
# +use_timestamp+ - whether to append an anti-caching timestamp to image URLs. Defaults to true
# +whiny+, +whiny_thumbnails+ - whether to raise when thumbnailing fails
# +use_default_time_zone+ - related to +use_timestamp+. Defaults to true
# +hash_digest+ - a string representing a class that will be used to hash URLs for obfuscation
# +hash_data+ - the relative URL for the hash data. This is interpolated using +interpolator+
# +hash_secret+ - a secret passed to the +hash_digest+
# +convert_options+ - flags passed to the +convert+ command for processing
# +source_file_options+ - flags passed to the +convert+ command that controls how the file is read
# +processors+ - classes that transform the attachment. Defaults to [:thumbnail]
# +preserve_files+ - whether to keep files on the filesystem when deleting to clearing the attachment. Defaults to false
# +interpolator+ - the object used to interpolate filenames and URLs. Defaults to Paperclip::Interpolations
# +url_generator+ - the object used to generate URLs, using the interpolator. Defaults to Paperclip::UrlGenerator
def initialize(name, instance, options = {})
@name = name
@instance = instance
......@@ -68,7 +72,8 @@ module Paperclip
@queued_for_write = {}
@errors = {}
@dirty = false
@interpolator = (options[:interpolator] || Paperclip::Interpolations)
@interpolator = options[:interpolator]
@url_generator = options[:url_generator].new(self, @options)
initialize_storage
end
......@@ -124,19 +129,36 @@ module Paperclip
uploaded_file.close if close_uploaded_file
end
# 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. Set use_timestamp to false
# if you want to stop the attachment update time appended to the url
# Returns the public URL of the attachment with a given style. This does
# not necessarily need to point to a file that your Web server can access
# and can instead point to an action in your app, for example for fine grained
# security; this has a serious performance tradeoff.
#
# Options:
#
# +timestamp+ - Add a timestamp to the end of the URL. Default: true.
# +escape+ - Perform URI escaping to the URL. Default: true.
#
# Global controls (set on has_attached_file):
#
# +interpolator+ - The object that fills in a URL pattern's variables.
# +default_url+ - The image to show when the attachment has no image.
# +url+ - The URL for a saved image.
# +url_generator+ - The object that generates a URL. Default: Paperclip::UrlGenerator.
#
# As mentioned just above, the object that generates this URL can be passed
# in, for finer control. This object must respond to two methods:
#
# +#new(Paperclip::Attachment, Paperclip::Options)+
# +#for(style_name, options_hash)+
def url(style_name = default_style, options = {})
options = handle_url_options(options)
url = interpolate(most_appropriate_url, style_name)
default_options = {:timestamp => @options.use_timestamp, :escape => true}
url = url_timestamp(url) if options[:timestamp]
url = escape_url(url) if options[:escape]
url
if options == true || options == false # Backwards compatibility.
@url_generator.for(style_name, default_options.merge(:timestamp => options))
else
@url_generator.for(style_name, default_options.merge(options))
end
end
# Returns the path of the attachment as defined by the :path option. If the
......@@ -328,44 +350,6 @@ module Paperclip
private
def handle_url_options(options)
timestamp = extract_timestamp(options)
options = {} if options == true || options == false
options[:timestamp] = timestamp
options[:escape] = true if options[:escape].nil?
options
end
def extract_timestamp(options)
possibilities = [((options == true || options == false) ? options : nil),
(options.respond_to?(:[]) ? options[:timestamp] : nil),
@options.use_timestamp]
possibilities.find{|n| !n.nil? }
end
def default_url
return @options.default_url.call(self) if @options.default_url.is_a?(Proc)
@options.default_url
end
def most_appropriate_url
if original_filename.nil?
default_url
else
@options.url
end
end
def url_timestamp(url)
return url unless updated_at
delimiter_char = url.include?("?") ? "&" : "?"
"#{url}#{delimiter_char}#{updated_at.to_s}"
end
def escape_url(url)
url.respond_to?(:escape) ? url.escape : URI.escape(url)
end
def ensure_required_accessors! #:nodoc:
%w(file_name).each do |field|
unless @instance.respond_to?("#{name}_#{field}") && @instance.respond_to?("#{name}_#{field}=")
......
......@@ -34,6 +34,9 @@ module Paperclip
@processors = hash[:processors]
@preserve_files = hash[:preserve_files]
@http_proxy = hash[:http_proxy]
@interpolator = hash[:interpolator]
@escape = hash[:escape]
@url_generator = hash[:url_generator]
#s3 options
@s3_credentials = hash[:s3_credentials]
......
require 'uri'
module Paperclip
class UrlGenerator
def initialize(attachment, attachment_options)
@attachment = attachment
@attachment_options = attachment_options
end
def for(style_name, options)
escape_url_as_needed(
timestamp_as_needed(
@attachment_options.interpolator.interpolate(most_appropriate_url, @attachment, style_name),
options
), options)
end
private
# This method is all over the place.
def default_url
if @attachment_options.default_url.respond_to?(:call)
@attachment_options.default_url.call(@attachment)
elsif @attachment_options.default_url.is_a?(Symbol)
@attachment.instance.send(@attachment_options.default_url)
else
@attachment_options.default_url
end
end
def most_appropriate_url
if @attachment.original_filename.nil?
default_url
else
@attachment_options.url
end
end
def timestamp_as_needed(url, options)
if options[:timestamp] && timestamp_possible?
delimiter_char = url.include?('?') ? '&' : '?'
"#{url}#{delimiter_char}#{@attachment.updated_at.to_s}"
else
url
end
end
def timestamp_possible?
@attachment.respond_to?(:updated_at) && @attachment.updated_at.present?
end
def escape_url_as_needed(url, options)
if options[:escape]
escape_url(url)
else
url
end
end
def escape_url(url)
url.respond_to?(:escape) ? url.escape : URI.escape(url)
end
end
end
......@@ -53,6 +53,10 @@ ActiveRecord::Base.logger = ActiveSupport::BufferedLogger.new(File.dirname(__FIL
ActiveRecord::Base.establish_connection(config['test'])
Paperclip.options[:logger] = ActiveRecord::Base.logger
Dir[File.join(File.dirname(__FILE__), 'support','*')].each do |f|
require f
end
def reset_class class_name
ActiveRecord::Base.send(:include, Paperclip::Glue)
Object.send(:remove_const, class_name) rescue nil
......
# encoding: utf-8
require './test/helper'
class MockAttachment < Struct.new(:one, :two)
class DSO < Struct.new(:one, :two)
def instance
self
end
......@@ -17,7 +17,7 @@ class OptionsTest < Test::Unit::TestCase
context "#styles with a plain hash" do
setup do
@attachment = MockAttachment.new(nil, nil)
@attachment = DSO.new(nil, nil)
@options = Paperclip::Options.new(@attachment,
:styles => {
:something => ["400x400", :png]
......@@ -35,7 +35,7 @@ class OptionsTest < Test::Unit::TestCase
context "#styles is a proc" do
setup do
@attachment = MockAttachment.new("123x456", :doc)
@attachment = DSO.new("123x456", :doc)
@options = Paperclip::Options.new(@attachment,
:styles => lambda {|att|
{:something => {:geometry => att.one, :format => att.two}}
......@@ -59,7 +59,7 @@ class OptionsTest < Test::Unit::TestCase
context "#processors" do
setup do
@attachment = MockAttachment.new(nil, nil)
@attachment = DSO.new(nil, nil)
end
should "return processors if not a proc" do
@options = Paperclip::Options.new(@attachment, :processors => [:one])
......
class MockAttachment
attr_accessor :updated_at, :original_filename
def initialize(options = {})
@model = options[:model]
@responds_to_updated_at = options[:responds_to_updated_at]
@updated_at = options[:updated_at]
@original_filename = options[:original_filename]
end
def instance
@model
end
def respond_to?(meth)
if meth.to_s == "updated_at"
@responds_to_updated_at || @updated_at
else
super
end
end
end
class MockInterpolator
def initialize(options = {})
@options = options
end
def interpolate(pattern, attachment, style_name)
@interpolated_pattern = pattern
@interpolated_attachment = attachment
@interpolated_style_name = style_name
@options[:result]
end
def has_interpolated_pattern?(pattern)
@interpolated_pattern == pattern
end
def has_interpolated_style_name?(style_name)
@interpolated_style_name == style_name
end
def has_interpolated_attachment?(attachment)
@interpolated_attachment == attachment
end
end
class MockModel
end
class MockUrlGeneratorBuilder
def initializer
end
def new(attachment, attachment_options)
@attachment = attachment
@attachment_options = attachment_options
self
end
def for(style_name, options)
@generated_url_with_style_name = style_name
@generated_url_with_options = options
"hello"
end
def has_generated_url_with_options?(options)
# options.is_a_subhash_of(@generated_url_with_options)
options.inject(true) do |acc,(k,v)|
acc && @generated_url_with_options[k] == v
end
end
def has_generated_url_with_style_name?(style_name)
@generated_url_with_style_name == style_name
end
end
# encoding: utf-8
require './test/helper'
require 'paperclip/url_generator'
require 'paperclip/options'
class UrlGeneratorTest < Test::Unit::TestCase
should "use the given interpolator" do
expected = "the expected result"
mock_attachment = MockAttachment.new
mock_interpolator = MockInterpolator.new(:result => expected)
url_generator = Paperclip::UrlGenerator.new(mock_attachment,
Paperclip::Options.new(mock_attachment, :interpolator => mock_interpolator))
result = url_generator.for(:style_name, {})
assert_equal expected, result
assert mock_interpolator.has_interpolated_attachment?(mock_attachment)
assert mock_interpolator.has_interpolated_style_name?(:style_name)
end
should "use the default URL when no file is assigned" do
mock_attachment = MockAttachment.new
mock_interpolator = MockInterpolator.new
default_url = "the default url"
options = Paperclip::Options.new(mock_attachment,
:interpolator => mock_interpolator,
:default_url => default_url)
url_generator = Paperclip::UrlGenerator.new(mock_attachment, options)
url_generator.for(:style_name, {})
assert mock_interpolator.has_interpolated_pattern?(default_url),
"expected the interpolator to be passed #{default_url.inspect} but it wasn't"
end
should "execute the default URL lambda when no file is assigned" do
mock_attachment = MockAttachment.new
mock_interpolator = MockInterpolator.new
default_url = lambda {|attachment| "the #{attachment.class.name} default url" }
options = Paperclip::Options.new(mock_attachment,
:interpolator => mock_interpolator,
:default_url => default_url)
url_generator = Paperclip::UrlGenerator.new(mock_attachment, options)
url_generator.for(:style_name, {})
assert mock_interpolator.has_interpolated_pattern?("the MockAttachment default url"),
%{expected the interpolator to be passed "the MockAttachment default url", but it wasn't}
end
should "execute the method named by the symbol as the default URL when no file is assigned" do
mock_model = MockModel.new
mock_attachment = MockAttachment.new(:model => mock_model)
mock_interpolator = MockInterpolator.new
default_url = :to_s
options = Paperclip::Options.new(mock_attachment,
:interpolator => mock_interpolator,
:default_url => default_url)
url_generator = Paperclip::UrlGenerator.new(mock_attachment, options)
url_generator.for(:style_name, {})
assert mock_interpolator.has_interpolated_pattern?(mock_model.to_s),
%{expected the interpolator to be passed #{mock_model.to_s}, but it wasn't}
end
should "URL-escape spaces if asked to" do
expected = "the expected result"
mock_attachment = MockAttachment.new
mock_interpolator = MockInterpolator.new(:result => expected)
options = Paperclip::Options.new(mock_attachment, :interpolator => mock_interpolator)
url_generator = Paperclip::UrlGenerator.new(mock_attachment, options)
result = url_generator.for(:style_name, {:escape => true})
assert_equal "the%20expected%20result", result
end
should "escape the result of the interpolator using a method on the object, if asked to escape" do
expected = Class.new do
def escape
"the escaped result"
end
end.new
mock_attachment = MockAttachment.new
mock_interpolator = MockInterpolator.new(:result => expected)
options = Paperclip::Options.new(mock_attachment, :interpolator => mock_interpolator)
url_generator = Paperclip::UrlGenerator.new(mock_attachment, options)
result = url_generator.for(:style_name, {:escape => true})
assert_equal "the escaped result", result
end
should "leave spaces unescaped as asked to" do
expected = "the expected result"
mock_attachment = MockAttachment.new
mock_interpolator = MockInterpolator.new(:result => expected)
options = Paperclip::Options.new(mock_attachment, :interpolator => mock_interpolator)
url_generator = Paperclip::UrlGenerator.new(mock_attachment, options)
result = url_generator.for(:style_name, {:escape => false})
assert_equal "the expected result", result
end
should "default to leaving spaces unescaped" do
expected = "the expected result"
mock_attachment = MockAttachment.new
mock_interpolator = MockInterpolator.new(:result => expected)
options = Paperclip::Options.new(mock_attachment, :interpolator => mock_interpolator)
url_generator = Paperclip::UrlGenerator.new(mock_attachment, options)
result = url_generator.for(:style_name, {})
assert_equal "the expected result", result
end
should "produce URLs without the updated_at value when the object does not respond to updated_at" do
expected = "the expected result"
mock_interpolator = MockInterpolator.new(:result => expected)
mock_attachment = MockAttachment.new(:responds_to_updated_at => false)
options = Paperclip::Options.new(mock_attachment, :interpolator => mock_interpolator)
url_generator = Paperclip::UrlGenerator.new(mock_attachment, options)
result = url_generator.for(:style_name, {:timestamp => true})
assert_equal expected, result
end
should "produce URLs without the updated_at value when the updated_at value is nil" do
expected = "the expected result"
mock_interpolator = MockInterpolator.new(:result => expected)
mock_attachment = MockAttachment.new(:responds_to_updated_at => true, :updated_at => nil)
options = Paperclip::Options.new(mock_attachment, :interpolator => mock_interpolator)
url_generator = Paperclip::UrlGenerator.new(mock_attachment, options)
result = url_generator.for(:style_name, {:timestamp => true})
assert_equal expected, result
end
should "produce URLs with the updated_at when it exists" do
expected = "the expected result"
updated_at = 1231231234
mock_interpolator = MockInterpolator.new(:result => expected)
mock_attachment = MockAttachment.new(:updated_at => updated_at)
options = Paperclip::Options.new(mock_attachment, :interpolator => mock_interpolator)
url_generator = Paperclip::UrlGenerator.new(mock_attachment, options)
result = url_generator.for(:style_name, {:timestamp => true})
assert_equal "#{expected}?#{updated_at}", result
end
should "produce URLs with the updated_at when it exists, separated with a & if a ? already exists" do
expected = "the?expected result"
updated_at = 1231231234
mock_interpolator = MockInterpolator.new(:result => expected)
mock_attachment = MockAttachment.new(:updated_at => updated_at)
options = Paperclip::Options.new(mock_attachment, :interpolator => mock_interpolator)
url_generator = Paperclip::UrlGenerator.new(mock_attachment, options)
result = url_generator.for(:style_name, {:timestamp => true})
assert_equal "#{expected}&#{updated_at}", result
end
should "produce URLs without the updated_at when told to do as much" do
expected = "the expected result"
updated_at = 1231231234
mock_interpolator = MockInterpolator.new(:result => expected)
mock_attachment = MockAttachment.new(:updated_at => updated_at)
options = Paperclip::Options.new(mock_attachment, :interpolator => mock_interpolator)
url_generator = Paperclip::UrlGenerator.new(mock_attachment, options)
result = url_generator.for(:style_name, {:timestamp => false})
assert_equal expected, result
end
should "produce the correct URL when the instance has a file name" do
expected = "the expected result"
mock_attachment = MockAttachment.new(:original_filename => 'exists')
mock_interpolator = MockInterpolator.new
options = Paperclip::Options.new(mock_attachment, :interpolator => mock_interpolator, :url => expected)
url_generator = Paperclip::UrlGenerator.new(mock_attachment, options)
url_generator.for(:style_name, {})
assert mock_interpolator.has_interpolated_pattern?(expected),
"expected the interpolator to be passed #{expected.inspect} but it wasn't"
end
end
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