beaucollins-paperclip 2.2.7

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.
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 +131 -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