paperclip-hacked 2.3.1.1

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 (48) hide show
  1. data/LICENSE +26 -0
  2. data/README.rdoc +179 -0
  3. data/Rakefile +103 -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 +357 -0
  9. data/lib/paperclip/attachmenta.rb +334 -0
  10. data/lib/paperclip/callback_compatability.rb +33 -0
  11. data/lib/paperclip/geometry.rb +115 -0
  12. data/lib/paperclip/interpolations.rb +108 -0
  13. data/lib/paperclip/iostream.rb +59 -0
  14. data/lib/paperclip/matchers.rb +33 -0
  15. data/lib/paperclip/matchers/have_attached_file_matcher.rb +57 -0
  16. data/lib/paperclip/matchers/validate_attachment_content_type_matcher.rb +75 -0
  17. data/lib/paperclip/matchers/validate_attachment_presence_matcher.rb +55 -0
  18. data/lib/paperclip/matchers/validate_attachment_size_matcher.rb +96 -0
  19. data/lib/paperclip/processor.rb +49 -0
  20. data/lib/paperclip/storage.rb +247 -0
  21. data/lib/paperclip/thumbnail.rb +75 -0
  22. data/lib/paperclip/upfile.rb +50 -0
  23. data/shoulda_macros/paperclip.rb +117 -0
  24. data/tasks/paperclip_tasks.rake +79 -0
  25. data/test/attachment_test.rb +764 -0
  26. data/test/database.yml +4 -0
  27. data/test/fixtures/12k.png +0 -0
  28. data/test/fixtures/50x50.png +0 -0
  29. data/test/fixtures/5k.png +0 -0
  30. data/test/fixtures/bad.png +1 -0
  31. data/test/fixtures/s3.yml +8 -0
  32. data/test/fixtures/text.txt +0 -0
  33. data/test/fixtures/twopage.pdf +0 -0
  34. data/test/geometry_test.rb +177 -0
  35. data/test/helper.rb +128 -0
  36. data/test/integration_test.rb +483 -0
  37. data/test/interpolations_test.rb +124 -0
  38. data/test/iostream_test.rb +78 -0
  39. data/test/matchers/have_attached_file_matcher_test.rb +24 -0
  40. data/test/matchers/validate_attachment_content_type_matcher_test.rb +37 -0
  41. data/test/matchers/validate_attachment_presence_matcher_test.rb +26 -0
  42. data/test/matchers/validate_attachment_size_matcher_test.rb +51 -0
  43. data/test/paperclip_test.rb +298 -0
  44. data/test/processor_test.rb +10 -0
  45. data/test/storage_test.rb +330 -0
  46. data/test/thumbnail_test.rb +227 -0
  47. data/test/upfile_test.rb +36 -0
  48. metadata +185 -0
@@ -0,0 +1,49 @@
1
+ module Paperclip
2
+ # Paperclip processors allow you to modify attached files when they are
3
+ # attached in any way you are able. Paperclip itself uses command-line
4
+ # programs for its included Thumbnail processor, but custom processors
5
+ # are not required to follow suit.
6
+ #
7
+ # Processors are required to be defined inside the Paperclip module and
8
+ # are also required to be a subclass of Paperclip::Processor. There is
9
+ # only one method you *must* implement to properly be a subclass:
10
+ # #make, but #initialize may also be of use. Both methods accept 3
11
+ # arguments: the file that will be operated on (which is an instance of
12
+ # File), a hash of options that were defined in has_attached_file's
13
+ # style hash, and the Paperclip::Attachmenta itself.
14
+ #
15
+ # All #make needs to return is an instance of File (Tempfile is
16
+ # acceptable) which contains the results of the processing.
17
+ #
18
+ # See Paperclip.run for more information about using command-line
19
+ # utilities from within Processors.
20
+ class Processor
21
+ attr_accessor :file, :options, :attachment
22
+
23
+ def initialize file, options = {}, attachment = nil
24
+ @file = file
25
+ @options = options
26
+ @attachment = attachment
27
+ end
28
+
29
+ def make
30
+ end
31
+
32
+ def self.make file, options = {}, attachment = nil
33
+ new(file, options, attachment).make
34
+ end
35
+ end
36
+
37
+ # Due to how ImageMagick handles its image format conversion and how Tempfile
38
+ # handles its naming scheme, it is necessary to override how Tempfile makes
39
+ # its names so as to allow for file extensions. Idea taken from the comments
40
+ # on this blog post:
41
+ # http://marsorange.com/archives/of-mogrify-ruby-tempfile-dynamic-class-definitions
42
+ class Tempfile < ::Tempfile
43
+ # Replaces Tempfile's +make_tmpname+ with one that honors file extensions.
44
+ def make_tmpname(basename, n)
45
+ extension = File.extname(basename)
46
+ sprintf("%s,%d,%d%s", File.basename(basename, extension), $$, n, extension)
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,247 @@
1
+ module Paperclip
2
+ module Storage
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::Attachmenta#interpolate for more information on variable interpolaton.
18
+ # :path => "/var/app/attachments/:class/:id/:style/:basename.:extension"
19
+ module Filesystem
20
+ def self.extended base
21
+ end
22
+
23
+ def exists?(style_name = default_style)
24
+ if original_filename
25
+ File.exist?(path(style_name))
26
+ else
27
+ false
28
+ end
29
+ end
30
+
31
+ # Returns representation of the data of the file assigned to the given
32
+ # style, in the format most representative of the current storage.
33
+ def to_file style_name = default_style
34
+ @queued_for_write[style_name] || (File.new(path(style_name), 'rb') if exists?(style_name))
35
+ end
36
+
37
+ def flush_writes #:nodoc:
38
+ @queued_for_write.each do |style_name, file|
39
+ file.close
40
+ FileUtils.mkdir_p(File.dirname(path(style_name)))
41
+ log("saving #{path(style_name)}")
42
+ FileUtils.mv(file.path, path(style_name))
43
+ FileUtils.chmod(0644, path(style_name))
44
+ end
45
+ @queued_for_write = {}
46
+ end
47
+
48
+ def flush_deletes #:nodoc:
49
+ @queued_for_delete.each do |path|
50
+ begin
51
+ log("deleting #{path}")
52
+ FileUtils.rm(path) if File.exist?(path)
53
+ rescue Errno::ENOENT => e
54
+ # ignore file-not-found, let everything else pass
55
+ end
56
+ begin
57
+ while(true)
58
+ path = File.dirname(path)
59
+ FileUtils.rmdir(path)
60
+ end
61
+ rescue Errno::EEXIST, Errno::ENOTEMPTY, Errno::ENOENT, Errno::EINVAL, Errno::ENOTDIR
62
+ # Stop trying to remove parent directories
63
+ rescue SystemCallError => e
64
+ log("There was an unexpected error while deleting directories: #{e.class}")
65
+ # Ignore it
66
+ end
67
+ end
68
+ @queued_for_delete = []
69
+ end
70
+ end
71
+
72
+ # Amazon's S3 file hosting service is a scalable, easy place to store files for
73
+ # distribution. You can find out more about it at http://aws.amazon.com/s3
74
+ # There are a few S3-specific options for has_attached_file:
75
+ # * +s3_credentials+: Takes a path, a File, or a Hash. The path (or File) must point
76
+ # to a YAML file containing the +access_key_id+ and +secret_access_key+ that Amazon
77
+ # gives you. You can 'environment-space' this just like you do to your
78
+ # database.yml file, so different environments can use different accounts:
79
+ # development:
80
+ # access_key_id: 123...
81
+ # secret_access_key: 123...
82
+ # test:
83
+ # access_key_id: abc...
84
+ # secret_access_key: abc...
85
+ # production:
86
+ # access_key_id: 456...
87
+ # secret_access_key: 456...
88
+ # This is not required, however, and the file may simply look like this:
89
+ # access_key_id: 456...
90
+ # secret_access_key: 456...
91
+ # In which case, those access keys will be used in all environments. You can also
92
+ # put your bucket name in this file, instead of adding it to the code directly.
93
+ # This is useful when you want the same account but a different bucket for
94
+ # development versus production.
95
+ # * +s3_permissions+: This is a String that should be one of the "canned" access
96
+ # policies that S3 provides (more information can be found here:
97
+ # http://docs.amazonwebservices.com/AmazonS3/2006-03-01/RESTAccessPolicy.html#RESTCannedAccessPolicies)
98
+ # The default for Paperclip is :public_read.
99
+ # * +s3_protocol+: The protocol for the URLs generated to your S3 assets. Can be either
100
+ # 'http' or 'https'. Defaults to 'http' when your :s3_permissions are :public_read (the
101
+ # default), and 'https' when your :s3_permissions are anything else.
102
+ # * +s3_headers+: A hash of headers such as {'Expires' => 1.year.from_now.httpdate}
103
+ # * +bucket+: This is the name of the S3 bucket that will store your files. Remember
104
+ # that the bucket must be unique across all of Amazon S3. If the bucket does not exist
105
+ # Paperclip will attempt to create it. The bucket name will not be interpolated.
106
+ # You can define the bucket as a Proc if you want to determine it's name at runtime.
107
+ # Paperclip will call that Proc with attachment as the only argument.
108
+ # * +s3_host_alias+: The fully-qualified domain name (FQDN) that is the alias to the
109
+ # S3 domain of your bucket. Used with the :s3_alias_url url interpolation. See the
110
+ # link in the +url+ entry for more information about S3 domains and buckets.
111
+ # * +url+: There are three options for the S3 url. You can choose to have the bucket's name
112
+ # placed domain-style (bucket.s3.amazonaws.com) or path-style (s3.amazonaws.com/bucket).
113
+ # Lastly, you can specify a CNAME (which requires the CNAME to be specified as
114
+ # :s3_alias_url. You can read more about CNAMEs and S3 at
115
+ # http://docs.amazonwebservices.com/AmazonS3/latest/index.html?VirtualHosting.html
116
+ # Normally, this won't matter in the slightest and you can leave the default (which is
117
+ # path-style, or :s3_path_url). But in some cases paths don't work and you need to use
118
+ # the domain-style (:s3_domain_url). Anything else here will be treated like path-style.
119
+ # NOTE: If you use a CNAME for use with CloudFront, you can NOT specify https as your
120
+ # :s3_protocol; This is *not supported* by S3/CloudFront. Finally, when using the host
121
+ # alias, the :bucket parameter is ignored, as the hostname is used as the bucket name
122
+ # by S3.
123
+ # * +path+: This is the key under the bucket in which the file will be stored. The
124
+ # URL will be constructed from the bucket and the path. This is what you will want
125
+ # to interpolate. Keys should be unique, like filenames, and despite the fact that
126
+ # S3 (strictly speaking) does not support directories, you can still use a / to
127
+ # separate parts of your file name.
128
+ module S3
129
+ def self.extended base
130
+ begin
131
+ require 'aws/s3'
132
+ rescue LoadError => e
133
+ e.message << " (You may need to install the aws-s3 gem)"
134
+ raise e
135
+ end
136
+
137
+ base.instance_eval do
138
+ @s3_credentials = parse_credentials(@options[:s3_credentials])
139
+ @bucket = @options[:bucket] || @s3_credentials[:bucket]
140
+ @bucket = @bucket.call(self) if @bucket.is_a?(Proc)
141
+ @s3_options = @options[:s3_options] || {}
142
+ @s3_permissions = @options[:s3_permissions] || :public_read
143
+ @s3_protocol = @options[:s3_protocol] || (@s3_permissions == :public_read ? 'http' : 'https')
144
+ @s3_headers = @options[:s3_headers] || {}
145
+ @s3_host_alias = @options[:s3_host_alias]
146
+ @url = ":s3_path_url" unless @url.to_s.match(/^:s3.*url$/)
147
+ AWS::S3::Base.establish_connection!( @s3_options.merge(
148
+ :access_key_id => @s3_credentials[:access_key_id],
149
+ :secret_access_key => @s3_credentials[:secret_access_key]
150
+ ))
151
+ end
152
+ Paperclip.interpolates(:s3_alias_url) do |attachment, style|
153
+ "#{attachment.s3_protocol}://#{attachment.s3_host_alias}/#{attachment.path(style).gsub(%r{^/}, "")}"
154
+ end
155
+ Paperclip.interpolates(:s3_path_url) do |attachment, style|
156
+ "#{attachment.s3_protocol}://s3.amazonaws.com/#{attachment.bucket_name}/#{attachment.path(style).gsub(%r{^/}, "")}"
157
+ end
158
+ Paperclip.interpolates(:s3_domain_url) do |attachment, style|
159
+ "#{attachment.s3_protocol}://#{attachment.bucket_name}.s3.amazonaws.com/#{attachment.path(style).gsub(%r{^/}, "")}"
160
+ end
161
+ end
162
+
163
+ def expiring_url(time = 3600)
164
+ AWS::S3::S3Object.url_for(path, bucket_name, :expires_in => time )
165
+ end
166
+
167
+ def bucket_name
168
+ @bucket
169
+ end
170
+
171
+ def s3_host_alias
172
+ @s3_host_alias
173
+ end
174
+
175
+ def parse_credentials creds
176
+ creds = find_credentials(creds).stringify_keys
177
+ (creds[RAILS_ENV] || creds).symbolize_keys
178
+ end
179
+
180
+ def exists?(style = default_style)
181
+ if original_filename
182
+ AWS::S3::S3Object.exists?(path(style), bucket_name)
183
+ else
184
+ false
185
+ end
186
+ end
187
+
188
+ def s3_protocol
189
+ @s3_protocol
190
+ end
191
+
192
+ # Returns representation of the data of the file assigned to the given
193
+ # style, in the format most representative of the current storage.
194
+ def to_file style = default_style
195
+ return @queued_for_write[style] if @queued_for_write[style]
196
+ file = Tempfile.new(path(style))
197
+ file.write(AWS::S3::S3Object.value(path(style), bucket_name))
198
+ file.rewind
199
+ return file
200
+ end
201
+
202
+ def flush_writes #:nodoc:
203
+ @queued_for_write.each do |style, file|
204
+ begin
205
+ log("saving #{path(style)}")
206
+ AWS::S3::S3Object.store(path(style),
207
+ file,
208
+ bucket_name,
209
+ {:content_type => instance_read(:content_type),
210
+ :access => @s3_permissions,
211
+ }.merge(@s3_headers))
212
+ rescue AWS::S3::ResponseError => e
213
+ raise
214
+ end
215
+ end
216
+ @queued_for_write = {}
217
+ end
218
+
219
+ def flush_deletes #:nodoc:
220
+ @queued_for_delete.each do |path|
221
+ begin
222
+ log("deleting #{path}")
223
+ AWS::S3::S3Object.delete(path, bucket_name)
224
+ rescue AWS::S3::ResponseError
225
+ # Ignore this.
226
+ end
227
+ end
228
+ @queued_for_delete = []
229
+ end
230
+
231
+ def find_credentials creds
232
+ case creds
233
+ when File
234
+ YAML::load(ERB.new(File.read(creds.path)).result)
235
+ when String
236
+ YAML::load(ERB.new(File.read(creds)).result)
237
+ when Hash
238
+ creds
239
+ else
240
+ raise ArgumentError, "Credentials are not a path, file, or hash."
241
+ end
242
+ end
243
+ private :find_credentials
244
+
245
+ end
246
+ end
247
+ end
@@ -0,0 +1,75 @@
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, :source_file_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
+
16
+ geometry = options[:geometry]
17
+ @file = file
18
+ @crop = geometry[-1,1] == '#'
19
+ @target_geometry = Geometry.parse geometry
20
+ @current_geometry = Geometry.from_file @file
21
+ @source_file_options = options[:source_file_options]
22
+ @convert_options = options[:convert_options]
23
+ @whiny = options[:whiny].nil? ? true : options[:whiny]
24
+ @format = options[:format]
25
+
26
+ @current_format = File.extname(@file.path)
27
+ @basename = File.basename(@file.path, @current_format)
28
+
29
+ end
30
+
31
+ # Returns true if the +target_geometry+ is meant to crop.
32
+ def crop?
33
+ @crop
34
+ end
35
+
36
+ # Returns true if the image is meant to make use of additional convert options.
37
+ def convert_options?
38
+ !@convert_options.nil? && !@convert_options.empty?
39
+ end
40
+
41
+ # Performs the conversion of the +file+ into a thumbnail. Returns the Tempfile
42
+ # that contains the new image.
43
+ def make
44
+ src = @file
45
+ dst = Tempfile.new([@basename, @format].compact.join("."))
46
+ dst.binmode
47
+
48
+ command = <<-end_command
49
+ #{ source_file_options }
50
+ "#{ File.expand_path(src.path) }[0]"
51
+ #{ transformation_command }
52
+ "#{ File.expand_path(dst.path) }"
53
+ end_command
54
+
55
+ begin
56
+ success = Paperclip.run("convert", command.gsub(/\s+/, " "))
57
+ rescue PaperclipCommandLineError
58
+ raise PaperclipError, "There was an error processing the thumbnail for #{@basename}" if @whiny
59
+ end
60
+
61
+ dst
62
+ end
63
+
64
+ # Returns the command ImageMagick's +convert+ needs to transform the image
65
+ # into the thumbnail.
66
+ def transformation_command
67
+ scale, crop = @current_geometry.transformation_to(@target_geometry, crop?)
68
+ trans = ""
69
+ trans << " -resize \"#{scale}\"" unless scale.nil? || scale.empty?
70
+ trans << " -crop \"#{crop}\" +repage" if crop
71
+ trans << " #{convert_options}" if convert_options?
72
+ trans
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,50 @@
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"jp(e|g|eg)" 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 "js" then "application/js"
17
+ when "csv", "xml", "css" then "text/#{type}"
18
+ else
19
+ Paperclip.run("file", "--mime-type #{self.path}").split(':').last.strip rescue "application/x-#{type}"
20
+ end
21
+ end
22
+
23
+ # Returns the file's normal name.
24
+ def original_filename
25
+ File.basename(self.path)
26
+ end
27
+
28
+ # Returns the size of the file.
29
+ def size
30
+ File.size(self)
31
+ end
32
+ end
33
+ end
34
+
35
+ if defined? StringIO
36
+ class StringIO
37
+ attr_accessor :original_filename, :content_type
38
+ def original_filename
39
+ @original_filename ||= "stringio.txt"
40
+ end
41
+ def content_type
42
+ @content_type ||= "text/plain"
43
+ end
44
+ end
45
+ end
46
+
47
+ class File #:nodoc:
48
+ include Paperclip::Upfile
49
+ end
50
+
@@ -0,0 +1,117 @@
1
+ require 'paperclip/matchers'
2
+ require 'action_controller'
3
+
4
+ module Paperclip
5
+ # =Paperclip Shoulda Macros
6
+ #
7
+ # These macros are intended for use with shoulda, and will be included into
8
+ # your tests automatically. All of the macros use the standard shoulda
9
+ # assumption that the name of the test is based on the name of the model
10
+ # you're testing (that is, UserTest is the test for the User model), and
11
+ # will load that class for testing purposes.
12
+ module Shoulda
13
+ include Matchers
14
+ # This will test whether you have defined your attachment correctly by
15
+ # checking for all the required fields exist after the definition of the
16
+ # attachment.
17
+ def should_have_attached_file name
18
+ klass = self.name.gsub(/Test$/, '').constantize
19
+ matcher = have_attached_file name
20
+ should matcher.description do
21
+ assert_accepts(matcher, klass)
22
+ end
23
+ end
24
+
25
+ # Tests for validations on the presence of the attachment.
26
+ def should_validate_attachment_presence name
27
+ klass = self.name.gsub(/Test$/, '').constantize
28
+ matcher = validate_attachment_presence name
29
+ should matcher.description do
30
+ assert_accepts(matcher, klass)
31
+ end
32
+ end
33
+
34
+ # Tests that you have content_type validations specified. There are two
35
+ # options, :valid and :invalid. Both accept an array of strings. The
36
+ # strings should be a list of content types which will pass and fail
37
+ # validation, respectively.
38
+ def should_validate_attachment_content_type name, options = {}
39
+ klass = self.name.gsub(/Test$/, '').constantize
40
+ valid = [options[:valid]].flatten
41
+ invalid = [options[:invalid]].flatten
42
+ matcher = validate_attachment_content_type(name).allowing(valid).rejecting(invalid)
43
+ should matcher.description do
44
+ assert_accepts(matcher, klass)
45
+ end
46
+ end
47
+
48
+ # Tests to ensure that you have file size validations turned on. You
49
+ # can pass the same options to this that you can to
50
+ # validate_attachment_file_size - :less_than, :greater_than, and :in.
51
+ # :less_than checks that a file is less than a certain size, :greater_than
52
+ # checks that a file is more than a certain size, and :in takes a Range or
53
+ # Array which specifies the lower and upper limits of the file size.
54
+ def should_validate_attachment_size name, options = {}
55
+ klass = self.name.gsub(/Test$/, '').constantize
56
+ min = options[:greater_than] || (options[:in] && options[:in].first) || 0
57
+ max = options[:less_than] || (options[:in] && options[:in].last) || (1.0/0)
58
+ range = (min..max)
59
+ matcher = validate_attachment_size(name).in(range)
60
+ should matcher.description do
61
+ assert_accepts(matcher, klass)
62
+ end
63
+ end
64
+
65
+ # Stubs the HTTP PUT for an attachment using S3 storage.
66
+ #
67
+ # @example
68
+ # stub_paperclip_s3('user', 'avatar', 'png')
69
+ def stub_paperclip_s3(model, attachment, extension)
70
+ definition = model.gsub(" ", "_").classify.constantize.
71
+ attachment_definitions[attachment.to_sym]
72
+
73
+ path = "http://s3.amazonaws.com/:id/#{definition[:path]}"
74
+ path.gsub!(/:([^\/\.]+)/) do |match|
75
+ "([^\/\.]+)"
76
+ end
77
+
78
+ begin
79
+ FakeWeb.register_uri(:put, Regexp.new(path), :body => "OK")
80
+ rescue NameError
81
+ raise NameError, "the stub_paperclip_s3 shoulda macro requires the fakeweb gem."
82
+ end
83
+ end
84
+
85
+ # Stub S3 and return a file for attachment. Best with Factory Girl.
86
+ # Uses a strict directory convention:
87
+ #
88
+ # features/support/paperclip
89
+ #
90
+ # This method is used by the Paperclip-provided Cucumber step:
91
+ #
92
+ # When I attach a "demo_tape" "mp3" file to a "band" on S3
93
+ #
94
+ # @example
95
+ # Factory.define :band_with_demo_tape, :parent => :band do |band|
96
+ # band.demo_tape { band.paperclip_fixture("band", "demo_tape", "png") }
97
+ # end
98
+ def paperclip_fixture(model, attachment, extension)
99
+ stub_paperclip_s3(model, attachment, extension)
100
+ base_path = File.join(File.dirname(__FILE__), "..", "..",
101
+ "features", "support", "paperclip")
102
+ File.new(File.join(base_path, model, "#{attachment}.#{extension}"))
103
+ end
104
+ end
105
+ end
106
+
107
+ class ActionController::Integration::Session #:nodoc:
108
+ include Paperclip::Shoulda
109
+ end
110
+
111
+ class Factory
112
+ include Paperclip::Shoulda #:nodoc:
113
+ end
114
+
115
+ class Test::Unit::TestCase #:nodoc:
116
+ extend Paperclip::Shoulda
117
+ end