fermion-paperclip 2.2.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. data/LICENSE +26 -0
  2. data/README.rdoc +172 -0
  3. data/Rakefile +77 -0
  4. data/generators/paperclip/USAGE +5 -0
  5. data/generators/paperclip/paperclip_generator.rb +27 -0
  6. data/generators/paperclip/templates/paperclip_migration.rb.erb +19 -0
  7. data/init.rb +1 -0
  8. data/lib/paperclip.rb +318 -0
  9. data/lib/paperclip/attachment.rb +404 -0
  10. data/lib/paperclip/callback_compatability.rb +33 -0
  11. data/lib/paperclip/geometry.rb +115 -0
  12. data/lib/paperclip/iostream.rb +58 -0
  13. data/lib/paperclip/matchers.rb +4 -0
  14. data/lib/paperclip/matchers/have_attached_file_matcher.rb +49 -0
  15. data/lib/paperclip/matchers/validate_attachment_content_type_matcher.rb +66 -0
  16. data/lib/paperclip/matchers/validate_attachment_presence_matcher.rb +48 -0
  17. data/lib/paperclip/matchers/validate_attachment_size_matcher.rb +83 -0
  18. data/lib/paperclip/processor.rb +48 -0
  19. data/lib/paperclip/storage.rb +236 -0
  20. data/lib/paperclip/thumbnail.rb +70 -0
  21. data/lib/paperclip/upfile.rb +48 -0
  22. data/shoulda_macros/paperclip.rb +68 -0
  23. data/tasks/paperclip_tasks.rake +79 -0
  24. data/test/attachment_test.rb +742 -0
  25. data/test/database.yml +4 -0
  26. data/test/fixtures/12k.png +0 -0
  27. data/test/fixtures/50x50.png +0 -0
  28. data/test/fixtures/5k.png +0 -0
  29. data/test/fixtures/bad.png +1 -0
  30. data/test/fixtures/text.txt +0 -0
  31. data/test/fixtures/twopage.pdf +0 -0
  32. data/test/geometry_test.rb +168 -0
  33. data/test/helper.rb +82 -0
  34. data/test/integration_test.rb +481 -0
  35. data/test/iostream_test.rb +71 -0
  36. data/test/matchers/have_attached_file_matcher_test.rb +21 -0
  37. data/test/matchers/validate_attachment_content_type_matcher_test.rb +30 -0
  38. data/test/matchers/validate_attachment_presence_matcher_test.rb +21 -0
  39. data/test/matchers/validate_attachment_size_matcher_test.rb +50 -0
  40. data/test/paperclip_test.rb +233 -0
  41. data/test/processor_test.rb +10 -0
  42. data/test/storage_test.rb +277 -0
  43. data/test/thumbnail_test.rb +177 -0
  44. metadata +137 -0
@@ -0,0 +1,70 @@
1
+ module Paperclip
2
+ # Handles thumbnailing images that are uploaded.
3
+ class Thumbnail < Processor
4
+
5
+ attr_accessor :current_geometry, :target_geometry, :format, :whiny, :convert_options
6
+
7
+ # Creates a Thumbnail object set to work on the +file+ given. It
8
+ # will attempt to transform the image into one defined by +target_geometry+
9
+ # which is a "WxH"-style string. +format+ will be inferred from the +file+
10
+ # unless specified. Thumbnail creation will raise no errors unless
11
+ # +whiny+ 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, options = {}, attachment = nil
14
+ super
15
+ geometry = options[:geometry]
16
+ @file = file
17
+ @crop = geometry[-1,1] == '#'
18
+ @target_geometry = Geometry.parse geometry
19
+ @current_geometry = Geometry.from_file @file
20
+ @convert_options = options[:convert_options]
21
+ @whiny = options[:whiny].nil? ? true : options[:whiny]
22
+ @format = options[:format]
23
+
24
+ @current_format = File.extname(@file.path)
25
+ @basename = File.basename(@file.path, @current_format)
26
+ end
27
+
28
+ # Returns true if the +target_geometry+ is meant to crop.
29
+ def crop?
30
+ @crop
31
+ end
32
+
33
+ # Returns true if the image is meant to make use of additional convert options.
34
+ def convert_options?
35
+ not @convert_options.blank?
36
+ end
37
+
38
+ # Performs the conversion of the +file+ into a thumbnail. Returns the Tempfile
39
+ # that contains the new image.
40
+ def make
41
+ src = @file
42
+ dst = Tempfile.new([@basename, @format].compact.join("."))
43
+ dst.binmode
44
+
45
+ command = <<-end_command
46
+ "#{ File.expand_path(src.path) }[0]"
47
+ #{ transformation_command }
48
+ "#{ File.expand_path(dst.path) }"
49
+ end_command
50
+
51
+ begin
52
+ success = Paperclip.run("convert", command.gsub(/\s+/, " "))
53
+ rescue PaperclipCommandLineError
54
+ raise PaperclipError, "There was an error processing the thumbnail for #{@basename}" if @whiny
55
+ end
56
+
57
+ dst
58
+ end
59
+
60
+ # Returns the command ImageMagick's +convert+ needs to transform the image
61
+ # into the thumbnail.
62
+ def transformation_command
63
+ scale, crop = @current_geometry.transformation_to(@target_geometry, crop?)
64
+ trans = "-resize \"#{scale}\""
65
+ trans << " -crop \"#{crop}\" +repage" if crop
66
+ trans << " #{convert_options}" if convert_options?
67
+ trans
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,48 @@
1
+ module Paperclip
2
+ # The Upfile module is a convenience module for adding uploaded-file-type methods
3
+ # to the +File+ class. Useful for testing.
4
+ # user.avatar = File.new("test/test_avatar.jpg")
5
+ module Upfile
6
+
7
+ # Infer the MIME-type of the file from the extension.
8
+ def content_type
9
+ type = (self.path.match(/\.(\w+)$/)[1] rescue "octet-stream").downcase
10
+ case 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}"
18
+ end
19
+ end
20
+
21
+ # Returns the file's normal name.
22
+ def original_filename
23
+ File.basename(self.path)
24
+ end
25
+
26
+ # Returns the size of the file.
27
+ def size
28
+ File.size(self)
29
+ end
30
+ end
31
+ end
32
+
33
+ if defined? StringIO
34
+ class StringIO
35
+ attr_accessor :original_filename, :content_type
36
+ def original_filename
37
+ @original_filename ||= "stringio.txt"
38
+ end
39
+ def content_type
40
+ @content_type ||= "text/plain"
41
+ end
42
+ end
43
+ end
44
+
45
+ class File #:nodoc:
46
+ include Paperclip::Upfile
47
+ end
48
+
@@ -0,0 +1,68 @@
1
+ require 'paperclip/matchers'
2
+
3
+ module Paperclip
4
+ # =Paperclip Shoulda Macros
5
+ #
6
+ # These macros are intended for use with shoulda, and will be included into
7
+ # your tests automatically. All of the macros use the standard shoulda
8
+ # assumption that the name of the test is based on the name of the model
9
+ # you're testing (that is, UserTest is the test for the User model), and
10
+ # will load that class for testing purposes.
11
+ module Shoulda
12
+ include Matchers
13
+ # This will test whether you have defined your attachment correctly by
14
+ # checking for all the required fields exist after the definition of the
15
+ # attachment.
16
+ def should_have_attached_file name
17
+ klass = self.name.gsub(/Test$/, '').constantize
18
+ matcher = have_attached_file name
19
+ should matcher.description do
20
+ assert_accepts(matcher, klass)
21
+ end
22
+ end
23
+
24
+ # Tests for validations on the presence of the attachment.
25
+ def should_validate_attachment_presence name
26
+ klass = self.name.gsub(/Test$/, '').constantize
27
+ matcher = validate_attachment_presence name
28
+ should matcher.description do
29
+ assert_accepts(matcher, klass)
30
+ end
31
+ end
32
+
33
+ # Tests that you have content_type validations specified. There are two
34
+ # options, :valid and :invalid. Both accept an array of strings. The
35
+ # strings should be a list of content types which will pass and fail
36
+ # validation, respectively.
37
+ def should_validate_attachment_content_type name, options = {}
38
+ klass = self.name.gsub(/Test$/, '').constantize
39
+ valid = [options[:valid]].flatten
40
+ invalid = [options[:invalid]].flatten
41
+ matcher = validate_attachment_content_type(name).allowing(valid).rejecting(invalid)
42
+ should matcher.description do
43
+ assert_accepts(matcher, klass)
44
+ end
45
+ end
46
+
47
+ # Tests to ensure that you have file size validations turned on. You
48
+ # can pass the same options to this that you can to
49
+ # validate_attachment_file_size - :less_than, :greater_than, and :in.
50
+ # :less_than checks that a file is less than a certain size, :greater_than
51
+ # checks that a file is more than a certain size, and :in takes a Range or
52
+ # Array which specifies the lower and upper limits of the file size.
53
+ def should_validate_attachment_size name, options = {}
54
+ klass = self.name.gsub(/Test$/, '').constantize
55
+ min = options[:greater_than] || (options[:in] && options[:in].first) || 0
56
+ max = options[:less_than] || (options[:in] && options[:in].last) || (1.0/0)
57
+ range = (min..max)
58
+ matcher = validate_attachment_size(name).in(range)
59
+ should matcher.description do
60
+ assert_accepts(matcher, klass)
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ class Test::Unit::TestCase #:nodoc:
67
+ extend Paperclip::Shoulda
68
+ end
@@ -0,0 +1,79 @@
1
+ def obtain_class
2
+ class_name = ENV['CLASS'] || ENV['class']
3
+ raise "Must specify CLASS" unless class_name
4
+ @klass = Object.const_get(class_name)
5
+ end
6
+
7
+ def obtain_attachments
8
+ name = ENV['ATTACHMENT'] || ENV['attachment']
9
+ raise "Class #{@klass.name} has no attachments specified" unless @klass.respond_to?(:attachment_definitions)
10
+ if !name.blank? && @klass.attachment_definitions.keys.include?(name)
11
+ [ name ]
12
+ else
13
+ @klass.attachment_definitions.keys
14
+ end
15
+ end
16
+
17
+ def for_all_attachments
18
+ klass = obtain_class
19
+ names = obtain_attachments
20
+ ids = klass.connection.select_values(klass.send(:construct_finder_sql, :select => 'id'))
21
+
22
+ ids.each do |id|
23
+ instance = klass.find(id)
24
+ names.each do |name|
25
+ result = if instance.send("#{ name }?")
26
+ yield(instance, name)
27
+ else
28
+ true
29
+ end
30
+ print result ? "." : "x"; $stdout.flush
31
+ end
32
+ end
33
+ puts " Done."
34
+ end
35
+
36
+ namespace :paperclip do
37
+ desc "Refreshes both metadata and thumbnails."
38
+ task :refresh => ["paperclip:refresh:metadata", "paperclip:refresh:thumbnails"]
39
+
40
+ namespace :refresh do
41
+ desc "Regenerates thumbnails for a given CLASS (and optional ATTACHMENT)."
42
+ task :thumbnails => :environment do
43
+ errors = []
44
+ for_all_attachments do |instance, name|
45
+ result = instance.send(name).reprocess!
46
+ errors << [instance.id, instance.errors] unless instance.errors.blank?
47
+ result
48
+ end
49
+ errors.each{|e| puts "#{e.first}: #{e.last.full_messages.inspect}" }
50
+ end
51
+
52
+ desc "Regenerates content_type/size metadata for a given CLASS (and optional ATTACHMENT)."
53
+ task :metadata => :environment do
54
+ for_all_attachments do |instance, name|
55
+ if file = instance.send(name).to_file
56
+ instance.send("#{name}_file_name=", instance.send("#{name}_file_name").strip)
57
+ instance.send("#{name}_content_type=", file.content_type.strip)
58
+ instance.send("#{name}_file_size=", file.size) if instance.respond_to?("#{name}_file_size")
59
+ instance.save(false)
60
+ else
61
+ true
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ desc "Cleans out invalid attachments. Useful after you've added new validations."
68
+ task :clean => :environment do
69
+ for_all_attachments do |instance, name|
70
+ instance.send(name).send(:validate)
71
+ if instance.send(name).valid?
72
+ true
73
+ else
74
+ instance.send("#{name}=", nil)
75
+ instance.save
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,742 @@
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"), 'rb')
81
+ @dummy.avatar = @file
82
+ end
83
+
84
+ teardown { @file.close }
85
+
86
+ should "make sure that they are interpolated correctly" do
87
+ assert_equal "1024.omg/1024-bbq/1024what/000/001/024.wtf", @dummy.avatar.path
88
+ end
89
+ end
90
+
91
+ context "An attachment with a :rails_env interpolation" do
92
+ setup do
93
+ @rails_env = "blah"
94
+ @id = 1024
95
+ rebuild_model :path => ":rails_env/:id.png"
96
+ @dummy = Dummy.new
97
+ @dummy.stubs(:id).returns(@id)
98
+ @file = StringIO.new(".")
99
+ @dummy.avatar = @file
100
+ end
101
+
102
+ should "return the proper path" do
103
+ temporary_rails_env(@rails_env) {
104
+ assert_equal "#{@rails_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
+ @dummy.avatar
121
+ end
122
+
123
+ should "report the correct options when sent #extra_options_for(:thumb)" do
124
+ assert_equal "-thumbnailize -do_stuff", @dummy.avatar.send(:extra_options_for, :thumb), @dummy.avatar.convert_options.inspect
125
+ end
126
+
127
+ should "report the correct options when sent #extra_options_for(:large)" do
128
+ assert_equal "-do_stuff", @dummy.avatar.send(:extra_options_for, :large)
129
+ end
130
+
131
+ before_should "call extra_options_for(:thumb/:large)" do
132
+ Paperclip::Attachment.any_instance.expects(:extra_options_for).with(:thumb)
133
+ Paperclip::Attachment.any_instance.expects(:extra_options_for).with(:large)
134
+ end
135
+ end
136
+
137
+ context "An attachment with :convert_options that is a proc" do
138
+ setup do
139
+ rebuild_model :styles => {
140
+ :thumb => "100x100",
141
+ :large => "400x400"
142
+ },
143
+ :convert_options => {
144
+ :all => lambda{|i| i.all },
145
+ :thumb => lambda{|i| i.thumb }
146
+ }
147
+ Dummy.class_eval do
148
+ def all; "-all"; end
149
+ def thumb; "-thumb"; end
150
+ end
151
+ @dummy = Dummy.new
152
+ @dummy.avatar
153
+ end
154
+
155
+ should "report the correct options when sent #extra_options_for(:thumb)" do
156
+ assert_equal "-thumb -all", @dummy.avatar.send(:extra_options_for, :thumb), @dummy.avatar.convert_options.inspect
157
+ end
158
+
159
+ should "report the correct options when sent #extra_options_for(:large)" do
160
+ assert_equal "-all", @dummy.avatar.send(:extra_options_for, :large)
161
+ end
162
+
163
+ before_should "call extra_options_for(:thumb/:large)" do
164
+ Paperclip::Attachment.any_instance.expects(:extra_options_for).with(:thumb)
165
+ Paperclip::Attachment.any_instance.expects(:extra_options_for).with(:large)
166
+ end
167
+ end
168
+
169
+ context "An attachment with :path that is a proc" do
170
+ setup do
171
+ rebuild_model :path => lambda{ |attachment| "path/#{attachment.instance.other}.:extension" }
172
+
173
+ @file = File.new(File.join(File.dirname(__FILE__),
174
+ "fixtures",
175
+ "5k.png"), 'rb')
176
+ @dummyA = Dummy.new(:other => 'a')
177
+ @dummyA.avatar = @file
178
+ @dummyB = Dummy.new(:other => 'b')
179
+ @dummyB.avatar = @file
180
+ end
181
+
182
+ teardown { @file.close }
183
+
184
+ should "return correct path" do
185
+ assert_equal "path/a.png", @dummyA.avatar.path
186
+ assert_equal "path/b.png", @dummyB.avatar.path
187
+ end
188
+ end
189
+
190
+ context "An attachment with :styles that is a proc" do
191
+ setup do
192
+ rebuild_model :styles => lambda{ |attachment| {:thumb => "50x50#", :large => "400x400"} }
193
+
194
+ @attachment = Dummy.new.avatar
195
+ end
196
+
197
+ should "have the correct geometry" do
198
+ assert_equal "50x50#", @attachment.styles[:thumb][:geometry]
199
+ end
200
+ end
201
+
202
+ context "An attachment with :url that is a proc" do
203
+ setup do
204
+ rebuild_model :url => lambda{ |attachment| "path/#{attachment.instance.other}.:extension" }
205
+
206
+ @file = File.new(File.join(File.dirname(__FILE__),
207
+ "fixtures",
208
+ "5k.png"), 'rb')
209
+ @dummyA = Dummy.new(:other => 'a')
210
+ @dummyA.avatar = @file
211
+ @dummyB = Dummy.new(:other => 'b')
212
+ @dummyB.avatar = @file
213
+ end
214
+
215
+ teardown { @file.close }
216
+
217
+ should "return correct url" do
218
+ assert_equal "path/a.png", @dummyA.avatar.url(:original, false)
219
+ assert_equal "path/b.png", @dummyB.avatar.url(:original, false)
220
+ end
221
+ end
222
+
223
+ geometry_specs = [
224
+ [ lambda{|z| "50x50#" }, :png ],
225
+ lambda{|z| "50x50#" },
226
+ { :geometry => lambda{|z| "50x50#" } }
227
+ ]
228
+ geometry_specs.each do |geometry_spec|
229
+ context "An attachment geometry like #{geometry_spec}" do
230
+ setup do
231
+ rebuild_model :styles => { :normal => geometry_spec }
232
+ @attachment = Dummy.new.avatar
233
+ end
234
+
235
+ should "not run the procs immediately" do
236
+ assert_kind_of Proc, @attachment.styles[:normal][:geometry]
237
+ end
238
+
239
+ context "when assigned" do
240
+ setup do
241
+ @file = StringIO.new(".")
242
+ @attachment.assign(@file)
243
+ end
244
+
245
+ should "have the correct geometry" do
246
+ assert_equal "50x50#", @attachment.styles[:normal][:geometry]
247
+ end
248
+ end
249
+ end
250
+ end
251
+
252
+ context "An attachment with both 'normal' and hash-style styles" do
253
+ setup do
254
+ rebuild_model :styles => {
255
+ :normal => ["50x50#", :png],
256
+ :hash => { :geometry => "50x50#", :format => :png }
257
+ }
258
+ @dummy = Dummy.new
259
+ @attachment = @dummy.avatar
260
+ end
261
+
262
+ [:processors, :whiny, :convert_options, :geometry, :format].each do |field|
263
+ should "have the same #{field} field" do
264
+ assert_equal @attachment.styles[:normal][field], @attachment.styles[:hash][field]
265
+ end
266
+ end
267
+ end
268
+
269
+ context "An attachment with :processors that is a proc" do
270
+ setup do
271
+ rebuild_model :styles => { :normal => '' }, :processors => lambda { |a| [ :test ] }
272
+ @attachment = Dummy.new.avatar
273
+ end
274
+
275
+ should "not run the proc immediately" do
276
+ assert_kind_of Proc, @attachment.styles[:normal][:processors]
277
+ end
278
+
279
+ context "when assigned" do
280
+ setup do
281
+ @attachment.assign(StringIO.new("."))
282
+ end
283
+
284
+ should "have the correct processors" do
285
+ assert_equal [ :test ], @attachment.styles[:normal][:processors]
286
+ end
287
+ end
288
+ end
289
+
290
+ context "An attachment with erroring processor" do
291
+ setup do
292
+ rebuild_model :processor => [:thumbnail], :styles => { :small => '' }, :whiny_thumbnails => true
293
+ @dummy = Dummy.new
294
+ Paperclip::Thumbnail.expects(:make).raises(Paperclip::PaperclipError, "cannot be processed.")
295
+ @file = StringIO.new("...")
296
+ @file.stubs(:to_tempfile).returns(@file)
297
+ @dummy.avatar = @file
298
+ end
299
+
300
+ should "correctly forward processing error message to the instance" do
301
+ @dummy.valid?
302
+ assert_contains @dummy.errors.full_messages, "Avatar cannot be processed."
303
+ end
304
+ end
305
+
306
+ context "An attachment with multiple processors" do
307
+ setup do
308
+ class Paperclip::Test < Paperclip::Processor; end
309
+ @style_params = { :once => {:one => 1, :two => 2} }
310
+ rebuild_model :processors => [:thumbnail, :test], :styles => @style_params
311
+ @dummy = Dummy.new
312
+ @file = StringIO.new("...")
313
+ @file.stubs(:to_tempfile).returns(@file)
314
+ Paperclip::Test.stubs(:make).returns(@file)
315
+ Paperclip::Thumbnail.stubs(:make).returns(@file)
316
+ end
317
+
318
+ context "when assigned" do
319
+ setup { @dummy.avatar = @file }
320
+
321
+ before_should "call #make on all specified processors" do
322
+ expected_params = @style_params[:once].merge({:processors => [:thumbnail, :test], :whiny => nil, :convert_options => ""})
323
+ Paperclip::Thumbnail.expects(:make).with(@file, expected_params, @dummy.avatar).returns(@file)
324
+ Paperclip::Test.expects(:make).with(@file, expected_params, @dummy.avatar).returns(@file)
325
+ end
326
+
327
+ before_should "call #make with attachment passed as third argument" do
328
+ expected_params = @style_params[:once].merge({:processors => [:thumbnail, :test], :whiny => nil, :convert_options => ""})
329
+ Paperclip::Test.expects(:make).with(@file, expected_params, @dummy.avatar).returns(@file)
330
+ end
331
+ end
332
+ end
333
+
334
+ context "An attachment with no processors defined" do
335
+ setup do
336
+ rebuild_model :processors => [], :styles => {:something => 1}
337
+ @dummy = Dummy.new
338
+ @file = StringIO.new("...")
339
+ end
340
+ should "raise when assigned to" do
341
+ assert_raises(RuntimeError){ @dummy.avatar = @file }
342
+ end
343
+ end
344
+
345
+ context "Assigning an attachment with post_process hooks" do
346
+ setup do
347
+ rebuild_model :styles => { :something => "100x100#" }
348
+ Dummy.class_eval do
349
+ before_avatar_post_process :do_before_avatar
350
+ after_avatar_post_process :do_after_avatar
351
+ before_post_process :do_before_all
352
+ after_post_process :do_after_all
353
+ def do_before_avatar; end
354
+ def do_after_avatar; end
355
+ def do_before_all; end
356
+ def do_after_all; end
357
+ end
358
+ @file = StringIO.new(".")
359
+ @file.stubs(:to_tempfile).returns(@file)
360
+ @dummy = Dummy.new
361
+ Paperclip::Thumbnail.stubs(:make).returns(@file)
362
+ @attachment = @dummy.avatar
363
+ end
364
+
365
+ should "call the defined callbacks when assigned" do
366
+ @dummy.expects(:do_before_avatar).with()
367
+ @dummy.expects(:do_after_avatar).with()
368
+ @dummy.expects(:do_before_all).with()
369
+ @dummy.expects(:do_after_all).with()
370
+ Paperclip::Thumbnail.expects(:make).returns(@file)
371
+ @dummy.avatar = @file
372
+ end
373
+
374
+ should "not cancel the processing if a before_post_process returns nil" do
375
+ @dummy.expects(:do_before_avatar).with().returns(nil)
376
+ @dummy.expects(:do_after_avatar).with()
377
+ @dummy.expects(:do_before_all).with().returns(nil)
378
+ @dummy.expects(:do_after_all).with()
379
+ Paperclip::Thumbnail.expects(:make).returns(@file)
380
+ @dummy.avatar = @file
381
+ end
382
+
383
+ should "cancel the processing if a before_post_process returns false" do
384
+ @dummy.expects(:do_before_avatar).never
385
+ @dummy.expects(:do_after_avatar).never
386
+ @dummy.expects(:do_before_all).with().returns(false)
387
+ @dummy.expects(:do_after_all).never
388
+ Paperclip::Thumbnail.expects(:make).never
389
+ @dummy.avatar = @file
390
+ end
391
+
392
+ should "cancel the processing if a before_avatar_post_process returns false" do
393
+ @dummy.expects(:do_before_avatar).with().returns(false)
394
+ @dummy.expects(:do_after_avatar).never
395
+ @dummy.expects(:do_before_all).with().returns(true)
396
+ @dummy.expects(:do_after_all).never
397
+ Paperclip::Thumbnail.expects(:make).never
398
+ @dummy.avatar = @file
399
+ end
400
+ end
401
+
402
+ context "Assigning an attachment" do
403
+ setup do
404
+ rebuild_model :styles => { :something => "100x100#" }
405
+ @file = StringIO.new(".")
406
+ @file.expects(:original_filename).returns("5k.png\n\n")
407
+ @file.expects(:content_type).returns("image/png\n\n")
408
+ @file.stubs(:to_tempfile).returns(@file)
409
+ @dummy = Dummy.new
410
+ Paperclip::Thumbnail.expects(:make).returns(@file)
411
+ @dummy.expects(:run_callbacks).with(:before_avatar_post_process, {:original => @file})
412
+ @dummy.expects(:run_callbacks).with(:before_post_process, {:original => @file})
413
+ @dummy.expects(:run_callbacks).with(:after_avatar_post_process, {:original => @file, :something => @file})
414
+ @dummy.expects(:run_callbacks).with(:after_post_process, {:original => @file, :something => @file})
415
+ @attachment = @dummy.avatar
416
+ @dummy.avatar = @file
417
+ end
418
+
419
+ should "strip whitespace from original_filename field" do
420
+ assert_equal "5k.png", @dummy.avatar.original_filename
421
+ end
422
+
423
+ should "strip whitespace from content_type field" do
424
+ assert_equal "image/png", @dummy.avatar.instance.avatar_content_type
425
+ end
426
+ end
427
+
428
+ context "Attachment with strange letters" do
429
+ setup do
430
+ rebuild_model
431
+
432
+ @not_file = mock
433
+ @tempfile = mock
434
+ @not_file.stubs(:nil?).returns(false)
435
+ @not_file.expects(:size).returns(10)
436
+ @tempfile.expects(:size).returns(10)
437
+ @not_file.expects(:to_tempfile).returns(@tempfile)
438
+ @not_file.expects(:original_filename).returns("sheep_say_bæ.png\r\n")
439
+ @not_file.expects(:content_type).returns("image/png\r\n")
440
+
441
+ @dummy = Dummy.new
442
+ @attachment = @dummy.avatar
443
+ @attachment.expects(:valid_assignment?).with(@not_file).returns(true)
444
+ @attachment.expects(:queue_existing_for_delete)
445
+ @attachment.expects(:post_process)
446
+ @attachment.expects(:valid?).returns(true)
447
+ @attachment.expects(:validate)
448
+ @dummy.avatar = @not_file
449
+ end
450
+
451
+ should "remove strange letters and replace with underscore (_)" do
452
+ assert_equal "sheep_say_b_.png", @dummy.avatar.original_filename
453
+ end
454
+
455
+ end
456
+
457
+ context "An attachment" do
458
+ setup do
459
+ Paperclip::Attachment.default_options.merge!({
460
+ :path => ":rails_root/tmp/:attachment/:class/:style/:id/:basename.:extension"
461
+ })
462
+ FileUtils.rm_rf("tmp")
463
+ rebuild_model
464
+ @instance = Dummy.new
465
+ @attachment = Paperclip::Attachment.new(:avatar, @instance)
466
+ @file = File.new(File.join(File.dirname(__FILE__),
467
+ "fixtures",
468
+ "5k.png"), 'rb')
469
+ end
470
+
471
+ teardown { @file.close }
472
+
473
+ should "raise if there are not the correct columns when you try to assign" do
474
+ @other_attachment = Paperclip::Attachment.new(:not_here, @instance)
475
+ assert_raises(Paperclip::PaperclipError) do
476
+ @other_attachment.assign(@file)
477
+ end
478
+ end
479
+
480
+ should "return its default_url when no file assigned" do
481
+ assert @attachment.to_file.nil?
482
+ assert_equal "/avatars/original/missing.png", @attachment.url
483
+ assert_equal "/avatars/blah/missing.png", @attachment.url(:blah)
484
+ end
485
+
486
+ should "return nil as path when no file assigned" do
487
+ assert @attachment.to_file.nil?
488
+ assert_equal nil, @attachment.path
489
+ assert_equal nil, @attachment.path(:blah)
490
+ end
491
+
492
+ context "with a file assigned in the database" do
493
+ setup do
494
+ @attachment.stubs(:instance_read).with(:file_name).returns("5k.png")
495
+ @attachment.stubs(:instance_read).with(:content_type).returns("image/png")
496
+ @attachment.stubs(:instance_read).with(:file_size).returns(12345)
497
+ now = Time.now
498
+ Time.stubs(:now).returns(now)
499
+ @attachment.stubs(:instance_read).with(:updated_at).returns(Time.now)
500
+ end
501
+
502
+ should "return a correct url even if the file does not exist" do
503
+ assert_nil @attachment.to_file
504
+ assert_match %r{^/system/avatars/#{@instance.id}/blah/5k\.png}, @attachment.url(:blah)
505
+ end
506
+
507
+ should "make sure the updated_at mtime is in the url if it is defined" do
508
+ assert_match %r{#{Time.now.to_i}$}, @attachment.url(:blah)
509
+ end
510
+
511
+ should "make sure the updated_at mtime is NOT in the url if false is passed to the url method" do
512
+ assert_no_match %r{#{Time.now.to_i}$}, @attachment.url(:blah, false)
513
+ end
514
+
515
+ context "with the updated_at field removed" do
516
+ setup do
517
+ @attachment.stubs(:instance_read).with(:updated_at).returns(nil)
518
+ end
519
+
520
+ should "only return the url without the updated_at when sent #url" do
521
+ assert_match "/avatars/#{@instance.id}/blah/5k.png", @attachment.url(:blah)
522
+ end
523
+ end
524
+
525
+ should "return the proper path when filename has a single .'s" do
526
+ assert_equal "./test/../tmp/avatars/dummies/original/#{@instance.id}/5k.png", @attachment.path
527
+ end
528
+
529
+ should "return the proper path when filename has multiple .'s" do
530
+ @attachment.stubs(:instance_read).with(:file_name).returns("5k.old.png")
531
+ assert_equal "./test/../tmp/avatars/dummies/original/#{@instance.id}/5k.old.png", @attachment.path
532
+ end
533
+
534
+ context "when expecting three styles" do
535
+ setup do
536
+ styles = {:styles => { :large => ["400x400", :png],
537
+ :medium => ["100x100", :gif],
538
+ :small => ["32x32#", :jpg]}}
539
+ @attachment = Paperclip::Attachment.new(:avatar,
540
+ @instance,
541
+ styles)
542
+ end
543
+
544
+ context "and assigned a file" do
545
+ setup do
546
+ now = Time.now
547
+ Time.stubs(:now).returns(now)
548
+ @attachment.assign(@file)
549
+ end
550
+
551
+ should "be dirty" do
552
+ assert @attachment.dirty?
553
+ end
554
+
555
+ context "and saved" do
556
+ setup do
557
+ @attachment.save
558
+ end
559
+
560
+ should "return the real url" do
561
+ file = @attachment.to_file
562
+ assert file
563
+ assert_match %r{^/system/avatars/#{@instance.id}/original/5k\.png}, @attachment.url
564
+ assert_match %r{^/system/avatars/#{@instance.id}/small/5k\.jpg}, @attachment.url(:small)
565
+ file.close
566
+ end
567
+
568
+ should "commit the files to disk" do
569
+ [:large, :medium, :small].each do |style|
570
+ io = @attachment.to_io(style)
571
+ assert File.exists?(io)
572
+ assert ! io.is_a?(::Tempfile)
573
+ io.close
574
+ end
575
+ end
576
+
577
+ should "save the files as the right formats and sizes" do
578
+ [[:large, 400, 61, "PNG"],
579
+ [:medium, 100, 15, "GIF"],
580
+ [:small, 32, 32, "JPEG"]].each do |style|
581
+ cmd = %Q[identify -format "%w %h %b %m" "#{@attachment.path(style.first)}"]
582
+ out = `#{cmd}`
583
+ width, height, size, format = out.split(" ")
584
+ assert_equal style[1].to_s, width.to_s
585
+ assert_equal style[2].to_s, height.to_s
586
+ assert_equal style[3].to_s, format.to_s
587
+ end
588
+ end
589
+
590
+ should "still have its #file attribute not be nil" do
591
+ assert ! (file = @attachment.to_file).nil?
592
+ file.close
593
+ end
594
+
595
+ context "and trying to delete" do
596
+ setup do
597
+ @existing_names = @attachment.styles.keys.collect do |style|
598
+ @attachment.path(style)
599
+ end
600
+ end
601
+
602
+ should "delete the files after assigning nil" do
603
+ @attachment.expects(:instance_write).with(:file_name, nil)
604
+ @attachment.expects(:instance_write).with(:content_type, nil)
605
+ @attachment.expects(:instance_write).with(:file_size, nil)
606
+ @attachment.expects(:instance_write).with(:updated_at, nil)
607
+ @attachment.assign nil
608
+ @attachment.save
609
+ @existing_names.each{|f| assert ! File.exists?(f) }
610
+ end
611
+
612
+ should "delete the files when you call #clear and #save" do
613
+ @attachment.expects(:instance_write).with(:file_name, nil)
614
+ @attachment.expects(:instance_write).with(:content_type, nil)
615
+ @attachment.expects(:instance_write).with(:file_size, nil)
616
+ @attachment.expects(:instance_write).with(:updated_at, nil)
617
+ @attachment.clear
618
+ @attachment.save
619
+ @existing_names.each{|f| assert ! File.exists?(f) }
620
+ end
621
+
622
+ should "delete the files when you call #delete" do
623
+ @attachment.expects(:instance_write).with(:file_name, nil)
624
+ @attachment.expects(:instance_write).with(:content_type, nil)
625
+ @attachment.expects(:instance_write).with(:file_size, nil)
626
+ @attachment.expects(:instance_write).with(:updated_at, nil)
627
+ @attachment.destroy
628
+ @existing_names.each{|f| assert ! File.exists?(f) }
629
+ end
630
+ end
631
+ end
632
+ end
633
+ end
634
+
635
+ end
636
+
637
+ context "when trying a nonexistant storage type" do
638
+ setup do
639
+ rebuild_model :storage => :not_here
640
+ end
641
+
642
+ should "not be able to find the module" do
643
+ assert_raise(NameError){ Dummy.new.avatar }
644
+ end
645
+ end
646
+ end
647
+
648
+ context "An attachment with only a avatar_file_name column" do
649
+ setup do
650
+ ActiveRecord::Base.connection.create_table :dummies, :force => true do |table|
651
+ table.column :avatar_file_name, :string
652
+ end
653
+ rebuild_class
654
+ @dummy = Dummy.new
655
+ @file = File.new(File.join(File.dirname(__FILE__), "fixtures", "5k.png"), 'rb')
656
+ end
657
+
658
+ teardown { @file.close }
659
+
660
+ should "not error when assigned an attachment" do
661
+ assert_nothing_raised { @dummy.avatar = @file }
662
+ end
663
+
664
+ should "return the time when sent #avatar_updated_at" do
665
+ now = Time.now
666
+ Time.stubs(:now).returns(now)
667
+ @dummy.avatar = @file
668
+ assert now, @dummy.avatar.updated_at
669
+ end
670
+
671
+ should "return nil when reloaded and sent #avatar_updated_at" do
672
+ @dummy.save
673
+ @dummy.reload
674
+ assert_nil @dummy.avatar.updated_at
675
+ end
676
+
677
+ should "return the right value when sent #avatar_file_size" do
678
+ @dummy.avatar = @file
679
+ assert_equal @file.size, @dummy.avatar.size
680
+ end
681
+
682
+ context "and avatar_updated_at column" do
683
+ setup do
684
+ ActiveRecord::Base.connection.add_column :dummies, :avatar_updated_at, :timestamp
685
+ rebuild_class
686
+ @dummy = Dummy.new
687
+ end
688
+
689
+ should "not error when assigned an attachment" do
690
+ assert_nothing_raised { @dummy.avatar = @file }
691
+ end
692
+
693
+ should "return the right value when sent #avatar_updated_at" do
694
+ now = Time.now
695
+ Time.stubs(:now).returns(now)
696
+ @dummy.avatar = @file
697
+ assert_equal now.to_i, @dummy.avatar.updated_at
698
+ end
699
+ end
700
+
701
+ context "and avatar_content_type column" do
702
+ setup do
703
+ ActiveRecord::Base.connection.add_column :dummies, :avatar_content_type, :string
704
+ rebuild_class
705
+ @dummy = Dummy.new
706
+ end
707
+
708
+ should "not error when assigned an attachment" do
709
+ assert_nothing_raised { @dummy.avatar = @file }
710
+ end
711
+
712
+ should "return the right value when sent #avatar_content_type" do
713
+ @dummy.avatar = @file
714
+ assert_equal "image/png", @dummy.avatar.content_type
715
+ end
716
+ end
717
+
718
+ context "and avatar_file_size column" do
719
+ setup do
720
+ ActiveRecord::Base.connection.add_column :dummies, :avatar_file_size, :integer
721
+ rebuild_class
722
+ @dummy = Dummy.new
723
+ end
724
+
725
+ should "not error when assigned an attachment" do
726
+ assert_nothing_raised { @dummy.avatar = @file }
727
+ end
728
+
729
+ should "return the right value when sent #avatar_file_size" do
730
+ @dummy.avatar = @file
731
+ assert_equal @file.size, @dummy.avatar.size
732
+ end
733
+
734
+ should "return the right value when saved, reloaded, and sent #avatar_file_size" do
735
+ @dummy.avatar = @file
736
+ @dummy.save
737
+ @dummy = Dummy.find(@dummy.id)
738
+ assert_equal @file.size, @dummy.avatar.size
739
+ end
740
+ end
741
+ end
742
+ end