dm-paperclip 2.1.2.1 → 2.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,7 +11,7 @@ module IOStream
11
11
 
12
12
  # Copies one read-able object from one place to another in blocks, obviating the need to load
13
13
  # the whole thing into memory. Defaults to 8k blocks. If this module is included in both
14
- # both StringIO and Tempfile, then either can have its data copied anywhere else without typing
14
+ # StringIO and Tempfile, then either can have its data copied anywhere else without typing
15
15
  # worries or memory overhead worries. Returns a File if a String is passed in as the destination
16
16
  # and returns the IO or Tempfile as passed in if one is sent as the destination.
17
17
  def stream_to path_or_file, in_blocks_of = 8192
@@ -1,6 +1,21 @@
1
1
  module Paperclip
2
2
  module Storage
3
3
 
4
+ # The default place to store attachments is in the filesystem. Files on the local
5
+ # filesystem can be very easily served by Apache without requiring a hit to your app.
6
+ # They also can be processed more easily after they've been saved, as they're just
7
+ # normal files. There is one Filesystem-specific option for has_attached_file.
8
+ # * +path+: The location of the repository of attachments on disk. This can (and, in
9
+ # almost all cases, should) be coordinated with the value of the +url+ option to
10
+ # allow files to be saved into a place where Apache can serve them without
11
+ # hitting your app. Defaults to
12
+ # ":rails_root/public/:attachment/:id/:style/:basename.:extension"
13
+ # By default this places the files in the app's public directory which can be served
14
+ # directly. If you are using capistrano for deployment, a good idea would be to
15
+ # make a symlink to the capistrano-created system directory from inside your app's
16
+ # public directory.
17
+ # See Paperclip::Attachment#interpolate for more information on variable interpolaton.
18
+ # :path => "/var/app/attachments/:class/:id/:style/:filename"
4
19
  module Filesystem
5
20
  def self.extended base
6
21
  end
@@ -21,8 +36,10 @@ module Paperclip
21
36
  alias_method :to_io, :to_file
22
37
 
23
38
  def flush_writes #:nodoc:
39
+ #logger.info("[paperclip] Writing files for #{name}")
24
40
  @queued_for_write.each do |style, file|
25
41
  FileUtils.mkdir_p(File.dirname(path(style)))
42
+ #logger.info("[paperclip] -> #{path(style)}")
26
43
  result = file.stream_to(path(style))
27
44
  file.close
28
45
  result.close
@@ -31,8 +48,10 @@ module Paperclip
31
48
  end
32
49
 
33
50
  def flush_deletes #:nodoc:
51
+ #logger.info("[paperclip] Deleting files for #{name}")
34
52
  @queued_for_delete.each do |path|
35
53
  begin
54
+ #logger.info("[paperclip] -> #{path}")
36
55
  FileUtils.rm(path) if File.exist?(path)
37
56
  rescue Errno::ENOENT => e
38
57
  # ignore file-not-found, let everything else pass
@@ -42,19 +61,67 @@ module Paperclip
42
61
  end
43
62
  end
44
63
 
64
+ # Amazon's S3 file hosting service is a scalable, easy place to store files for
65
+ # distribution. You can find out more about it at http://aws.amazon.com/s3
66
+ # There are a few S3-specific options for has_attached_file:
67
+ # * +s3_credentials+: Takes a path, a File, or a Hash. The path (or File) must point
68
+ # to a YAML file containing the +access_key_id+ and +secret_access_key+ that Amazon
69
+ # gives you. You can 'environment-space' this just like you do to your
70
+ # database.yml file, so different environments can use different accounts:
71
+ # development:
72
+ # access_key_id: 123...
73
+ # secret_access_key: 123...
74
+ # test:
75
+ # access_key_id: abc...
76
+ # secret_access_key: abc...
77
+ # production:
78
+ # access_key_id: 456...
79
+ # secret_access_key: 456...
80
+ # This is not required, however, and the file may simply look like this:
81
+ # access_key_id: 456...
82
+ # secret_access_key: 456...
83
+ # In which case, those access keys will be used in all environments. You can also
84
+ # put your bucket name in this file, instead of adding it to the code directly.
85
+ # This is useful when you want the same account but a different bucket for
86
+ # development versus production.
87
+ # * +s3_permissions+: This is a String that should be one of the "canned" access
88
+ # policies that S3 provides (more information can be found here:
89
+ # http://docs.amazonwebservices.com/AmazonS3/2006-03-01/RESTAccessPolicy.html#RESTCannedAccessPolicies)
90
+ # The default for Paperclip is "public-read".
91
+ # * +s3_protocol+: The protocol for the URLs generated to your S3 assets. Can be either
92
+ # 'http' or 'https'. Defaults to 'http' when your :s3_permissions are 'public-read' (the
93
+ # default), and 'https' when your :s3_permissions are anything else.
94
+ # * +bucket+: This is the name of the S3 bucket that will store your files. Remember
95
+ # that the bucket must be unique across all of Amazon S3. If the bucket does not exist
96
+ # Paperclip will attempt to create it. The bucket name will not be interpolated.
97
+ # * +url+: There are two options for the S3 url. You can choose to have the bucket's name
98
+ # placed domain-style (bucket.s3.amazonaws.com) or path-style (s3.amazonaws.com/bucket).
99
+ # Normally, this won't matter in the slightest and you can leave the default (which is
100
+ # path-style, or :s3_path_url). But in some cases paths don't work and you need to use
101
+ # the domain-style (:s3_domain_url). Anything else here will be treated like path-style.
102
+ # * +path+: This is the key under the bucket in which the file will be stored. The
103
+ # URL will be constructed from the bucket and the path. This is what you will want
104
+ # to interpolate. Keys should be unique, like filenames, and despite the fact that
105
+ # S3 (strictly speaking) does not support directories, you can still use a / to
106
+ # separate parts of your file name.
45
107
  module S3
46
108
  def self.extended base
47
109
  require 'right_aws'
48
110
  base.instance_eval do
49
- @bucket = @options[:bucket]
50
- @s3_credentials = parse_credentials(@options[:s3_credentials])
51
- @s3_options = @options[:s3_options] || {}
52
- @s3_permissions = @options[:s3_permissions] || 'public-read'
53
- @url = ":s3_url"
111
+ @s3_credentials = parse_credentials(@options[:s3_credentials])
112
+ @bucket = @options[:bucket] || @s3_credentials[:bucket]
113
+ @s3_options = @options[:s3_options] || {}
114
+ @s3_permissions = @options[:s3_permissions] || 'public-read'
115
+ @s3_protocol = @options[:s3_protocol] || (@s3_permissions == 'public-read' ? 'http' : 'https')
116
+ @url = ":s3_path_url" unless @url.to_s.match(/^:s3.*url$/)
54
117
  end
55
- base.class.interpolations[:s3_url] = lambda do |attachment, style|
56
- "https://s3.amazonaws.com/#{attachment.bucket_name}/#{attachment.path(style).gsub(%r{^/}, "")}"
118
+ base.class.interpolations[:s3_path_url] = lambda do |attachment, style|
119
+ "#{attachment.s3_protocol}://s3.amazonaws.com/#{attachment.bucket_name}/#{attachment.path(style).gsub(%r{^/}, "")}"
57
120
  end
121
+ base.class.interpolations[:s3_domain_url] = lambda do |attachment, style|
122
+ "#{attachment.s3_protocol}://#{attachment.bucket_name}.s3.amazonaws.com/#{attachment.path(style).gsub(%r{^/}, "")}"
123
+ end
124
+ #ActiveRecord::Base.logger.info("[paperclip] S3 Storage Initalized.")
58
125
  end
59
126
 
60
127
  def s3
@@ -80,6 +147,10 @@ module Paperclip
80
147
  s3_bucket.key(path(style)) ? true : false
81
148
  end
82
149
 
150
+ def s3_protocol
151
+ @s3_protocol
152
+ end
153
+
83
154
  # Returns representation of the data of the file assigned to the given
84
155
  # style, in the format most representative of the current storage.
85
156
  def to_file style = default_style
@@ -88,8 +159,10 @@ module Paperclip
88
159
  alias_method :to_io, :to_file
89
160
 
90
161
  def flush_writes #:nodoc:
162
+ #logger.info("[paperclip] Writing files for #{name}")
91
163
  @queued_for_write.each do |style, file|
92
164
  begin
165
+ #logger.info("[paperclip] -> #{path(style)}")
93
166
  key = s3_bucket.key(path(style))
94
167
  key.data = file
95
168
  key.put(nil, @s3_permissions)
@@ -101,8 +174,10 @@ module Paperclip
101
174
  end
102
175
 
103
176
  def flush_deletes #:nodoc:
177
+ #logger.info("[paperclip] Writing files for #{name}")
104
178
  @queued_for_delete.each do |path|
105
179
  begin
180
+ #logger.info("[paperclip] -> #{path}")
106
181
  if file = s3_bucket.key(path)
107
182
  file.delete
108
183
  end
@@ -2,18 +2,20 @@ module Paperclip
2
2
  # Handles thumbnailing images that are uploaded.
3
3
  class Thumbnail
4
4
 
5
- attr_accessor :file, :current_geometry, :target_geometry, :format, :whiny_thumbnails
5
+ attr_accessor :file, :current_geometry, :target_geometry, :format, :whiny_thumbnails, :convert_options
6
6
 
7
7
  # Creates a Thumbnail object set to work on the +file+ given. It
8
8
  # will attempt to transform the image into one defined by +target_geometry+
9
9
  # which is a "WxH"-style string. +format+ will be inferred from the +file+
10
10
  # unless specified. Thumbnail creation will raise no errors unless
11
- # +whiny_thumbnails+ is true (which it is, by default.
12
- def initialize file, target_geometry, format = nil, whiny_thumbnails = true
11
+ # +whiny_thumbnails+ is true (which it is, by default. If +convert_options+ is
12
+ # set, the options will be appended to the convert command upon image conversion
13
+ def initialize file, target_geometry, format = nil, convert_options = nil, whiny_thumbnails = true
13
14
  @file = file
14
15
  @crop = target_geometry[-1,1] == '#'
15
16
  @target_geometry = Geometry.parse target_geometry
16
17
  @current_geometry = Geometry.from_file file
18
+ @convert_options = convert_options
17
19
  @whiny_thumbnails = whiny_thumbnails
18
20
 
19
21
  @current_format = File.extname(@file.path)
@@ -24,14 +26,19 @@ module Paperclip
24
26
 
25
27
  # Creates a thumbnail, as specified in +initialize+, +make+s it, and returns the
26
28
  # resulting Tempfile.
27
- def self.make file, dimensions, format = nil, whiny_thumbnails = true
28
- new(file, dimensions, format, whiny_thumbnails).make
29
+ def self.make file, dimensions, format = nil, convert_options = nil, whiny_thumbnails = true
30
+ new(file, dimensions, format, convert_options, whiny_thumbnails).make
29
31
  end
30
32
 
31
33
  # Returns true if the +target_geometry+ is meant to crop.
32
34
  def crop?
33
35
  @crop
34
36
  end
37
+
38
+ # Returns true if the image is meant to make use of additional convert options.
39
+ def convert_options?
40
+ not @convert_options.blank?
41
+ end
35
42
 
36
43
  # Performs the conversion of the +file+ into a thumbnail. Returns the Tempfile
37
44
  # that contains the new image.
@@ -42,13 +49,13 @@ module Paperclip
42
49
 
43
50
  command = <<-end_command
44
51
  #{ Paperclip.path_for_command('convert') }
45
- "#{ File.expand_path(src.path) }"
52
+ "#{ File.expand_path(src.path) }[0]"
46
53
  #{ transformation_command }
47
54
  "#{ File.expand_path(dst.path) }"
48
55
  end_command
49
56
  success = system(command.gsub(/\s+/, " "))
50
57
 
51
- if success && $?.exitstatus != 0 && @whiny_thumbnails
58
+ if !success && $?.exitstatus != 0 && @whiny_thumbnails
52
59
  raise PaperclipError, "There was an error processing this thumbnail"
53
60
  end
54
61
 
@@ -59,8 +66,9 @@ module Paperclip
59
66
  # into the thumbnail.
60
67
  def transformation_command
61
68
  scale, crop = @current_geometry.transformation_to(@target_geometry, crop?)
62
- trans = "-scale \"#{scale}\""
69
+ trans = "-resize \"#{scale}\""
63
70
  trans << " -crop \"#{crop}\" +repage" if crop
71
+ trans << " #{convert_options}" if convert_options?
64
72
  trans
65
73
  end
66
74
  end
@@ -6,12 +6,15 @@ module Paperclip
6
6
 
7
7
  # Infer the MIME-type of the file from the extension.
8
8
  def content_type
9
- type = self.path.match(/\.(\w+)$/)[1] rescue "octet-stream"
9
+ type = (self.path.match(/\.(\w+)$/)[1] rescue "octet-stream").downcase
10
10
  case type
11
- when "jpg", "png", "gif" then "image/#{type}"
12
- when "txt" then "text/plain"
13
- when "csv", "xml", "html", "htm", "css", "js" then "text/#{type}"
14
- else "x-application/#{type}"
11
+ when %r"jpe?g" then "image/jpeg"
12
+ when %r"tiff?" then "image/tiff"
13
+ when %r"png", "gif", "bmp" then "image/#{type}"
14
+ when "txt" then "text/plain"
15
+ when %r"html?" then "text/html"
16
+ when "csv", "xml", "css", "js" then "text/#{type}"
17
+ else "application/x-#{type}"
15
18
  end
16
19
  end
17
20
 
@@ -16,9 +16,9 @@ module Paperclip
16
16
  return true if @options[:in].include? field_value.to_i
17
17
 
18
18
  error_message ||= @options[:message] unless @options[:message].nil?
19
- error_message ||= "%s must be less than %s bytes".t(DataMapper::Inflection.humanize(@field_name), @options[:less_than]) unless @options[:less_than].nil?
20
- error_message ||= "%s must be greater than %s bytes".t(DataMapper::Inflection.humanize(@field_name), @options[:greater_than]) unless @options[:greater_than].nil?
21
- error_message ||= "%s must be between %s and %s bytes".t(DataMapper::Inflection.humanize(@field_name), @options[:in].first, @options[:in].last)
19
+ error_message ||= "%s must be less than %s bytes".t(Extlib::Inflection.humanize(@field_name), @options[:less_than]) unless @options[:less_than].nil?
20
+ error_message ||= "%s must be greater than %s bytes".t(Extlib::Inflection.humanize(@field_name), @options[:greater_than]) unless @options[:greater_than].nil?
21
+ error_message ||= "%s must be between %s and %s bytes".t(Extlib::Inflection.humanize(@field_name), @options[:in].first, @options[:in].last)
22
22
  add_error(target, error_message , @field_name)
23
23
  return false
24
24
  end
@@ -33,7 +33,7 @@ module Paperclip
33
33
  def call(target)
34
34
  field_value = target.validation_property_value(@field_name)
35
35
  if field_value.nil? || field_value.original_filename.blank?
36
- error_message = @options[:message] || "%s must be set".t(DataMapper::Inflection.humanize(@field_name))
36
+ error_message = @options[:message] || "%s must be set".t(Extlib::Inflection.humanize(@field_name))
37
37
  add_error(target, error_message , @field_name)
38
38
  return false
39
39
  end
@@ -56,7 +56,7 @@ module Paperclip
56
56
  content_type = target.validation_property_value(:"#{@field_name}_content_type")
57
57
  unless valid_types.any?{|t| t === content_type }
58
58
  error_message ||= @options[:message] unless @options[:message].nil?
59
- error_message ||= "%s's content type of '%s' is not a valid content type".t(DataMapper::Inflection.humanize(@field_name), content_type)
59
+ error_message ||= "%s's content type of '%s' is not a valid content type".t(Extlib::Inflection.humanize(@field_name), content_type)
60
60
  add_error(target, error_message , @field_name)
61
61
  return false
62
62
  end
@@ -0,0 +1,363 @@
1
+ require 'test/helper'
2
+
3
+ class Dummy
4
+ # This is a dummy class
5
+ end
6
+
7
+ class AttachmentTest < Test::Unit::TestCase
8
+ context "Attachment default_options" do
9
+ setup do
10
+ rebuild_model
11
+ @old_default_options = Paperclip::Attachment.default_options.dup
12
+ @new_default_options = @old_default_options.merge({
13
+ :path => "argle/bargle",
14
+ :url => "fooferon",
15
+ :default_url => "not here.png"
16
+ })
17
+ end
18
+
19
+ teardown do
20
+ Paperclip::Attachment.default_options.merge! @old_default_options
21
+ end
22
+
23
+ should "be overrideable" do
24
+ Paperclip::Attachment.default_options.merge!(@new_default_options)
25
+ @new_default_options.keys.each do |key|
26
+ assert_equal @new_default_options[key],
27
+ Paperclip::Attachment.default_options[key]
28
+ end
29
+ end
30
+
31
+ context "without an Attachment" do
32
+ setup do
33
+ @dummy = Dummy.new
34
+ end
35
+
36
+ should "return false when asked exists?" do
37
+ assert !@dummy.avatar.exists?
38
+ end
39
+ end
40
+
41
+ context "on an Attachment" do
42
+ setup do
43
+ @dummy = Dummy.new
44
+ @attachment = @dummy.avatar
45
+ end
46
+
47
+ Paperclip::Attachment.default_options.keys.each do |key|
48
+ should "be the default_options for #{key}" do
49
+ assert_equal @old_default_options[key],
50
+ @attachment.instance_variable_get("@#{key}"),
51
+ key
52
+ end
53
+ end
54
+
55
+ context "when redefined" do
56
+ setup do
57
+ Paperclip::Attachment.default_options.merge!(@new_default_options)
58
+ @dummy = Dummy.new
59
+ @attachment = @dummy.avatar
60
+ end
61
+
62
+ Paperclip::Attachment.default_options.keys.each do |key|
63
+ should "be the new default_options for #{key}" do
64
+ assert_equal @new_default_options[key],
65
+ @attachment.instance_variable_get("@#{key}"),
66
+ key
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+
73
+ context "An attachment with similarly named interpolations" do
74
+ setup do
75
+ rebuild_model :path => ":id.omg/:id-bbq/:idwhat/:id_partition.wtf"
76
+ @dummy = Dummy.new
77
+ @dummy.stubs(:id).returns(1024)
78
+ @file = File.new(File.join(File.dirname(__FILE__),
79
+ "fixtures",
80
+ "5k.png"))
81
+ @dummy.avatar = @file
82
+ end
83
+
84
+ should "make sure that they are interpolated correctly" do
85
+ assert_equal "1024.omg/1024-bbq/1024what/000/001/024.wtf", @dummy.avatar.path
86
+ end
87
+ end
88
+
89
+ context "An attachment with a :rails_env interpolation" do
90
+ setup do
91
+ @rails_env = "blah"
92
+ @id = 1024
93
+ rebuild_model :path => ":merb_env/:id.png"
94
+ @dummy = Dummy.new
95
+ @dummy.stubs(:id).returns(@id)
96
+ @file = File.new(File.join(File.dirname(__FILE__),
97
+ "fixtures",
98
+ "5k.png"))
99
+ @dummy.avatar = @file
100
+ end
101
+
102
+ should "return the proper path" do
103
+ temporary_env(@rails_env) {
104
+ assert_equal "#{Merb.env}/#{@id}.png", @dummy.avatar.path
105
+ }
106
+ end
107
+ end
108
+
109
+ context "An attachment with :convert_options" do
110
+ setup do
111
+ rebuild_model :styles => {
112
+ :thumb => "100x100",
113
+ :large => "400x400"
114
+ },
115
+ :convert_options => {
116
+ :all => "-do_stuff",
117
+ :thumb => "-thumbnailize"
118
+ }
119
+ @dummy = Dummy.new
120
+ end
121
+
122
+ should "report the correct options when sent #extra_options_for(:thumb)" do
123
+ assert_equal "-thumbnailize -do_stuff", @dummy.avatar.send(:extra_options_for, :thumb), @dummy.avatar.convert_options.inspect
124
+ end
125
+
126
+ should "report the correct options when sent #extra_options_for(:large)" do
127
+ assert_equal "-do_stuff", @dummy.avatar.send(:extra_options_for, :large)
128
+ end
129
+
130
+ context "when given a file" do
131
+ setup do
132
+ @file = File.new(File.join(File.dirname(__FILE__),
133
+ "fixtures",
134
+ "5k.png"))
135
+ Paperclip::Thumbnail.stubs(:make)
136
+ [:thumb, :large].each do |style|
137
+ @dummy.avatar.stubs(:extra_options_for).with(style)
138
+ end
139
+ end
140
+
141
+ [:thumb, :large].each do |style|
142
+ should "call extra_options_for(#{style})" do
143
+ @dummy.avatar.expects(:extra_options_for).with(style)
144
+ @dummy.avatar = @file
145
+ end
146
+ end
147
+ end
148
+ end
149
+
150
+ context "Assigning an attachment" do
151
+ setup do
152
+ rebuild_model
153
+
154
+ @not_file = mock
155
+ @not_file.stubs(:nil?).returns(false)
156
+ @not_file.expects(:to_tempfile).returns(self)
157
+ @not_file.expects(:original_filename).returns("filename.png\r\n")
158
+ @not_file.expects(:content_type).returns("image/png\r\n")
159
+ @not_file.expects(:size).returns(10)
160
+
161
+ @dummy = Dummy.new
162
+ @attachment = @dummy.avatar
163
+ @attachment.expects(:valid_assignment?).with(@not_file).returns(true)
164
+ @attachment.expects(:queue_existing_for_delete)
165
+ @attachment.expects(:post_process)
166
+ @attachment.expects(:validate)
167
+ @dummy.avatar = @not_file
168
+ end
169
+
170
+ should "strip whitespace from original_filename field" do
171
+ assert_equal "filename.png", @dummy.avatar.original_filename
172
+ end
173
+
174
+ should "strip whitespace from content_type field" do
175
+ assert_equal "image/png", @dummy.avatar.instance.avatar_content_type
176
+ end
177
+
178
+ end
179
+
180
+ context "Attachment with strange letters" do
181
+ setup do
182
+ rebuild_model
183
+
184
+ @not_file = mock
185
+ @not_file.stubs(:nil?).returns(false)
186
+ @not_file.expects(:to_tempfile).returns(self)
187
+ @not_file.expects(:original_filename).returns("sheep_say_b�.png\r\n")
188
+ @not_file.expects(:content_type).returns("image/png\r\n")
189
+ @not_file.expects(:size).returns(10)
190
+
191
+ @dummy = Dummy.new
192
+ @attachment = @dummy.avatar
193
+ @attachment.expects(:valid_assignment?).with(@not_file).returns(true)
194
+ @attachment.expects(:queue_existing_for_delete)
195
+ @attachment.expects(:post_process)
196
+ @attachment.expects(:validate)
197
+ @dummy.avatar = @not_file
198
+ end
199
+
200
+ should "remove strange letters and replace with underscore (_)" do
201
+ assert_equal "sheep_say_b_.png", @dummy.avatar.original_filename
202
+ end
203
+
204
+ end
205
+
206
+ context "An attachment" do
207
+ setup do
208
+ Paperclip::Attachment.default_options.merge!({
209
+ :path => ":merb_root/tmp/:attachment/:class/:style/:id/:basename.:extension"
210
+ })
211
+ FileUtils.rm_rf("tmp")
212
+ rebuild_model
213
+ @instance = Dummy.new
214
+ @instance.stubs(:id).returns(1024)
215
+ @attachment = Paperclip::Attachment.new(:avatar, @instance)
216
+ @file = File.new(File.join(File.dirname(__FILE__),
217
+ "fixtures",
218
+ "5k.png"))
219
+ end
220
+
221
+ should "raise if there are not the correct columns when you try to assign" do
222
+ @other_attachment = Paperclip::Attachment.new(:not_here, @instance)
223
+ assert_raises(Paperclip::PaperclipError) do
224
+ @other_attachment.assign(@file)
225
+ end
226
+ end
227
+
228
+ should "return its default_url when no file assigned" do
229
+ assert @attachment.to_file.nil?
230
+ assert_equal "/avatars/original/missing.png", @attachment.url
231
+ assert_equal "/avatars/blah/missing.png", @attachment.url(:blah)
232
+ end
233
+
234
+ context "with a file assigned in the database" do
235
+ setup do
236
+ @instance.stubs(:attribute_get).with(:avatar_file_name).returns('5k.png')
237
+ @instance.stubs(:attribute_get).with(:avatar_content_type).returns("image/png")
238
+ @instance.stubs(:attribute_get).with(:avatar_file_size).returns(12345)
239
+ now = Time.now
240
+ Time.stubs(:now).returns(now)
241
+ @instance.stubs(:attribute_get).with(:avatar_updated_at).returns(Time.now)
242
+ end
243
+
244
+ should "return a correct url even if the file does not exist" do
245
+ assert_nil @attachment.to_file
246
+ assert_match %r{^/avatars/#{@instance.id}/blah/5k\.png}, @attachment.url(:blah)
247
+ end
248
+
249
+ should "make sure the updated_at mtime is in the url if it is defined" do
250
+ time = Time.now
251
+ assert_match %r{#{time.year}#{time.month}#{time.day}#{time.hour}#{time.min}#{time.sec}$}, @attachment.url(:blah)
252
+ end
253
+
254
+ context "with the updated_at field removed" do
255
+ setup do
256
+ @instance.stubs(:[]).with(:avatar_updated_at).returns(nil)
257
+ end
258
+
259
+ should "only return the url without the updated_at when sent #url" do
260
+ assert_match "/avatars/#{@instance.id}/blah/5k.png", @attachment.url(:blah)
261
+ end
262
+ end
263
+
264
+ should "return the proper path when filename has a single .'s" do
265
+ assert_equal "./test/../tmp/avatars/dummies/original/#{@instance.id}/5k.png", @attachment.path
266
+ end
267
+
268
+ should "return the proper path when filename has multiple .'s" do
269
+ @instance.stubs(:attribute_get).with(:avatar_file_name).returns("5k.old.png")
270
+ assert_equal "./test/../tmp/avatars/dummies/original/#{@instance.id}/5k.old.png", @attachment.path
271
+ end
272
+
273
+ context "when expecting three styles" do
274
+ setup do
275
+ styles = {:styles => { :large => ["400x400", :png],
276
+ :medium => ["100x100", :gif],
277
+ :small => ["32x32#", :jpg]}}
278
+ @attachment = Paperclip::Attachment.new(:avatar,
279
+ @instance,
280
+ styles)
281
+ end
282
+
283
+ context "and assigned a file" do
284
+ setup do
285
+ now = Time.now
286
+ Time.stubs(:now).returns(now)
287
+ @attachment.assign(@file)
288
+ end
289
+
290
+ should "be dirty" do
291
+ assert @attachment.dirty?
292
+ end
293
+
294
+ context "and saved" do
295
+ setup do
296
+ @attachment.save
297
+ end
298
+
299
+ should "return the real url" do
300
+ assert @attachment.to_file
301
+ assert_match %r{^/avatars/#{@instance.id}/original/5k\.png}, @attachment.url
302
+ assert_match %r{^/avatars/#{@instance.id}/small/5k\.jpg}, @attachment.url(:small)
303
+ end
304
+
305
+ should "commit the files to disk" do
306
+ [:large, :medium, :small].each do |style|
307
+ io = @attachment.to_io(style)
308
+ assert File.exists?(io)
309
+ assert ! io.is_a?(::Tempfile)
310
+ end
311
+ end
312
+
313
+ should "save the files as the right formats and sizes" do
314
+ [[:large, 400, 61, "PNG"],
315
+ [:medium, 100, 15, "GIF"],
316
+ [:small, 32, 32, "JPEG"]].each do |style|
317
+ cmd = "identify -format '%w %h %b %m' " +
318
+ "#{@attachment.to_io(style.first).path}"
319
+ out = `#{cmd}`
320
+ width, height, size, format = out.split(" ")
321
+ assert_equal style[1].to_s, width.to_s
322
+ assert_equal style[2].to_s, height.to_s
323
+ assert_equal style[3].to_s, format.to_s
324
+ end
325
+ end
326
+
327
+ should "still have its #file attribute not be nil" do
328
+ assert ! @attachment.to_file.nil?
329
+ end
330
+
331
+ context "and deleted" do
332
+ setup do
333
+ @existing_names = @attachment.styles.keys.collect do |style|
334
+ @attachment.path(style)
335
+ end
336
+ @instance.expects(:attributes=).with({ :avatar_file_name => nil,
337
+ :avatar_content_type => nil,
338
+ :avatar_file_size => nil })
339
+ @attachment.assign nil
340
+ @attachment.save
341
+ end
342
+
343
+ should "delete the files" do
344
+ @existing_names.each{|f| assert ! File.exists?(f) }
345
+ end
346
+ end
347
+ end
348
+ end
349
+ end
350
+
351
+ end
352
+
353
+ context "when trying a nonexistant storage type" do
354
+ setup do
355
+ rebuild_model :storage => :not_here
356
+ end
357
+
358
+ should "not be able to find the module" do
359
+ assert_raise(NameError){ Dummy.new.avatar }
360
+ end
361
+ end
362
+ end
363
+ end