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 # encoding: utf-8
require 'uri' require 'uri'
require 'paperclip/url_generator'
module Paperclip module Paperclip
# The Attachment class manages the files for a given attachment. It saves # The Attachment class manages the files for a given attachment. It saves
...@@ -25,7 +26,9 @@ module Paperclip ...@@ -25,7 +26,9 @@ module Paperclip
:use_default_time_zone => true, :use_default_time_zone => true,
:hash_digest => "SHA1", :hash_digest => "SHA1",
:hash_data => ":class/:attachment/:id/:style/:updated_at", :hash_data => ":class/:attachment/:id/:style/:updated_at",
:preserve_files => false :preserve_files => false,
:interpolator => Paperclip::Interpolations,
:url_generator => Paperclip::UrlGenerator
} }
end end
...@@ -56,7 +59,8 @@ module Paperclip ...@@ -56,7 +59,8 @@ module Paperclip
# +processors+ - classes that transform the attachment. Defaults to [:thumbnail] # +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 # +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 # +interpolator+ - the object used to interpolate filenames and URLs. Defaults to Paperclip::Interpolations
def initialize name, instance, options = {} # +url_generator+ - the object used to generate URLs, using the interpolator. Defaults to Paperclip::UrlGenerator
def initialize(name, instance, options = {})
@name = name @name = name
@instance = instance @instance = instance
...@@ -68,7 +72,8 @@ module Paperclip ...@@ -68,7 +72,8 @@ module Paperclip
@queued_for_write = {} @queued_for_write = {}
@errors = {} @errors = {}
@dirty = false @dirty = false
@interpolator = (options[:interpolator] || Paperclip::Interpolations) @interpolator = options[:interpolator]
@url_generator = options[:url_generator].new(self, @options)
initialize_storage initialize_storage
end end
...@@ -124,19 +129,36 @@ module Paperclip ...@@ -124,19 +129,36 @@ module Paperclip
uploaded_file.close if close_uploaded_file uploaded_file.close if close_uploaded_file
end end
# Returns the public URL of the attachment, with a given style. Note that # Returns the public URL of the attachment with a given style. This does
# this does not necessarily need to point to a file that your web server # not necessarily need to point to a file that your Web server can access
# can access and can point to an action in your app, if you need fine # and can instead point to an action in your app, for example for fine grained
# grained security. This is not recommended if you don't need the # security; this has a serious performance tradeoff.
# security, however, for performance reasons. Set use_timestamp to false #
# if you want to stop the attachment update time appended to the url # 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 = {}) def url(style_name = default_style, options = {})
options = handle_url_options(options) default_options = {:timestamp => @options.use_timestamp, :escape => true}
url = interpolate(most_appropriate_url, style_name)
url = url_timestamp(url) if options[:timestamp] if options == true || options == false # Backwards compatibility.
url = escape_url(url) if options[:escape] @url_generator.for(style_name, default_options.merge(:timestamp => options))
url else
@url_generator.for(style_name, default_options.merge(options))
end
end end
# Returns the path of the attachment as defined by the :path option. If the # Returns the path of the attachment as defined by the :path option. If the
...@@ -328,44 +350,6 @@ module Paperclip ...@@ -328,44 +350,6 @@ module Paperclip
private 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: def ensure_required_accessors! #:nodoc:
%w(file_name).each do |field| %w(file_name).each do |field|
unless @instance.respond_to?("#{name}_#{field}") && @instance.respond_to?("#{name}_#{field}=") unless @instance.respond_to?("#{name}_#{field}") && @instance.respond_to?("#{name}_#{field}=")
......
...@@ -34,6 +34,9 @@ module Paperclip ...@@ -34,6 +34,9 @@ module Paperclip
@processors = hash[:processors] @processors = hash[:processors]
@preserve_files = hash[:preserve_files] @preserve_files = hash[:preserve_files]
@http_proxy = hash[:http_proxy] @http_proxy = hash[:http_proxy]
@interpolator = hash[:interpolator]
@escape = hash[:escape]
@url_generator = hash[:url_generator]
#s3 options #s3 options
@s3_credentials = hash[:s3_credentials] @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 ...@@ -53,6 +53,10 @@ ActiveRecord::Base.logger = ActiveSupport::BufferedLogger.new(File.dirname(__FIL
ActiveRecord::Base.establish_connection(config['test']) ActiveRecord::Base.establish_connection(config['test'])
Paperclip.options[:logger] = ActiveRecord::Base.logger Paperclip.options[:logger] = ActiveRecord::Base.logger
Dir[File.join(File.dirname(__FILE__), 'support','*')].each do |f|
require f
end
def reset_class class_name def reset_class class_name
ActiveRecord::Base.send(:include, Paperclip::Glue) ActiveRecord::Base.send(:include, Paperclip::Glue)
Object.send(:remove_const, class_name) rescue nil Object.send(:remove_const, class_name) rescue nil
......
# encoding: utf-8 # encoding: utf-8
require './test/helper' require './test/helper'
class MockAttachment < Struct.new(:one, :two) class DSO < Struct.new(:one, :two)
def instance def instance
self self
end end
...@@ -17,7 +17,7 @@ class OptionsTest < Test::Unit::TestCase ...@@ -17,7 +17,7 @@ class OptionsTest < Test::Unit::TestCase
context "#styles with a plain hash" do context "#styles with a plain hash" do
setup do setup do
@attachment = MockAttachment.new(nil, nil) @attachment = DSO.new(nil, nil)
@options = Paperclip::Options.new(@attachment, @options = Paperclip::Options.new(@attachment,
:styles => { :styles => {
:something => ["400x400", :png] :something => ["400x400", :png]
...@@ -35,7 +35,7 @@ class OptionsTest < Test::Unit::TestCase ...@@ -35,7 +35,7 @@ class OptionsTest < Test::Unit::TestCase
context "#styles is a proc" do context "#styles is a proc" do
setup do setup do
@attachment = MockAttachment.new("123x456", :doc) @attachment = DSO.new("123x456", :doc)
@options = Paperclip::Options.new(@attachment, @options = Paperclip::Options.new(@attachment,
:styles => lambda {|att| :styles => lambda {|att|
{:something => {:geometry => att.one, :format => att.two}} {:something => {:geometry => att.one, :format => att.two}}
...@@ -59,7 +59,7 @@ class OptionsTest < Test::Unit::TestCase ...@@ -59,7 +59,7 @@ class OptionsTest < Test::Unit::TestCase
context "#processors" do context "#processors" do
setup do setup do
@attachment = MockAttachment.new(nil, nil) @attachment = DSO.new(nil, nil)
end end
should "return processors if not a proc" do should "return processors if not a proc" do
@options = Paperclip::Options.new(@attachment, :processors => [:one]) @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