cloudfuji_paperclip 2.4.6

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 (105) hide show
  1. data/.gitignore +22 -0
  2. data/.travis.yml +13 -0
  3. data/Appraisals +14 -0
  4. data/CONTRIBUTING.md +38 -0
  5. data/Gemfile +5 -0
  6. data/Gemfile.lock +137 -0
  7. data/LICENSE +26 -0
  8. data/README.md +444 -0
  9. data/Rakefile +41 -0
  10. data/cucumber/paperclip_steps.rb +6 -0
  11. data/features/basic_integration.feature +46 -0
  12. data/features/rake_tasks.feature +63 -0
  13. data/features/step_definitions/attachment_steps.rb +65 -0
  14. data/features/step_definitions/html_steps.rb +15 -0
  15. data/features/step_definitions/rails_steps.rb +182 -0
  16. data/features/step_definitions/s3_steps.rb +14 -0
  17. data/features/step_definitions/web_steps.rb +209 -0
  18. data/features/support/env.rb +8 -0
  19. data/features/support/fakeweb.rb +3 -0
  20. data/features/support/fixtures/.boot_config.rb.swo +0 -0
  21. data/features/support/fixtures/boot_config.txt +15 -0
  22. data/features/support/fixtures/gemfile.txt +5 -0
  23. data/features/support/fixtures/preinitializer.txt +20 -0
  24. data/features/support/paths.rb +28 -0
  25. data/features/support/rails.rb +46 -0
  26. data/features/support/selectors.rb +19 -0
  27. data/gemfiles/rails2.gemfile +9 -0
  28. data/gemfiles/rails3.gemfile +9 -0
  29. data/gemfiles/rails3_1.gemfile +9 -0
  30. data/generators/paperclip/USAGE +5 -0
  31. data/generators/paperclip/paperclip_generator.rb +27 -0
  32. data/generators/paperclip/templates/paperclip_migration.rb.erb +19 -0
  33. data/init.rb +4 -0
  34. data/lib/generators/paperclip/USAGE +8 -0
  35. data/lib/generators/paperclip/paperclip_generator.rb +33 -0
  36. data/lib/generators/paperclip/templates/paperclip_migration.rb.erb +19 -0
  37. data/lib/paperclip/attachment.rb +468 -0
  38. data/lib/paperclip/callback_compatibility.rb +61 -0
  39. data/lib/paperclip/geometry.rb +120 -0
  40. data/lib/paperclip/interpolations.rb +174 -0
  41. data/lib/paperclip/iostream.rb +45 -0
  42. data/lib/paperclip/matchers/have_attached_file_matcher.rb +57 -0
  43. data/lib/paperclip/matchers/validate_attachment_content_type_matcher.rb +81 -0
  44. data/lib/paperclip/matchers/validate_attachment_presence_matcher.rb +54 -0
  45. data/lib/paperclip/matchers/validate_attachment_size_matcher.rb +95 -0
  46. data/lib/paperclip/matchers.rb +64 -0
  47. data/lib/paperclip/missing_attachment_styles.rb +87 -0
  48. data/lib/paperclip/processor.rb +58 -0
  49. data/lib/paperclip/railtie.rb +31 -0
  50. data/lib/paperclip/schema.rb +39 -0
  51. data/lib/paperclip/storage/filesystem.rb +81 -0
  52. data/lib/paperclip/storage/fog.rb +174 -0
  53. data/lib/paperclip/storage/s3.rb +333 -0
  54. data/lib/paperclip/storage.rb +3 -0
  55. data/lib/paperclip/style.rb +103 -0
  56. data/lib/paperclip/thumbnail.rb +105 -0
  57. data/lib/paperclip/upfile.rb +62 -0
  58. data/lib/paperclip/url_generator.rb +64 -0
  59. data/lib/paperclip/version.rb +3 -0
  60. data/lib/paperclip.rb +487 -0
  61. data/lib/tasks/paperclip.rake +101 -0
  62. data/paperclip.gemspec +41 -0
  63. data/rails/init.rb +2 -0
  64. data/shoulda_macros/paperclip.rb +124 -0
  65. data/test/.gitignore +1 -0
  66. data/test/attachment_test.rb +1116 -0
  67. data/test/database.yml +4 -0
  68. data/test/fixtures/12k.png +0 -0
  69. data/test/fixtures/50x50.png +0 -0
  70. data/test/fixtures/5k.png +0 -0
  71. data/test/fixtures/animated.gif +0 -0
  72. data/test/fixtures/bad.png +1 -0
  73. data/test/fixtures/fog.yml +8 -0
  74. data/test/fixtures/question?mark.png +0 -0
  75. data/test/fixtures/s3.yml +8 -0
  76. data/test/fixtures/spaced file.png +0 -0
  77. data/test/fixtures/text.txt +1 -0
  78. data/test/fixtures/twopage.pdf +0 -0
  79. data/test/fixtures/uppercase.PNG +0 -0
  80. data/test/geometry_test.rb +206 -0
  81. data/test/helper.rb +177 -0
  82. data/test/integration_test.rb +654 -0
  83. data/test/interpolations_test.rb +216 -0
  84. data/test/iostream_test.rb +71 -0
  85. data/test/matchers/have_attached_file_matcher_test.rb +24 -0
  86. data/test/matchers/validate_attachment_content_type_matcher_test.rb +87 -0
  87. data/test/matchers/validate_attachment_presence_matcher_test.rb +26 -0
  88. data/test/matchers/validate_attachment_size_matcher_test.rb +51 -0
  89. data/test/paperclip_missing_attachment_styles_test.rb +80 -0
  90. data/test/paperclip_test.rb +390 -0
  91. data/test/processor_test.rb +10 -0
  92. data/test/schema_test.rb +98 -0
  93. data/test/storage/filesystem_test.rb +56 -0
  94. data/test/storage/fog_test.rb +219 -0
  95. data/test/storage/s3_live_test.rb +138 -0
  96. data/test/storage/s3_test.rb +943 -0
  97. data/test/style_test.rb +209 -0
  98. data/test/support/mock_attachment.rb +22 -0
  99. data/test/support/mock_interpolator.rb +24 -0
  100. data/test/support/mock_model.rb +2 -0
  101. data/test/support/mock_url_generator_builder.rb +27 -0
  102. data/test/thumbnail_test.rb +383 -0
  103. data/test/upfile_test.rb +53 -0
  104. data/test/url_generator_test.rb +187 -0
  105. metadata +404 -0
@@ -0,0 +1,120 @@
1
+ module Paperclip
2
+
3
+ # Defines the geometry of an image.
4
+ class Geometry
5
+ attr_accessor :height, :width, :modifier
6
+
7
+ # Gives a Geometry representing the given height and width
8
+ def initialize width = nil, height = nil, modifier = nil
9
+ @height = height.to_f
10
+ @width = width.to_f
11
+ @modifier = modifier
12
+ end
13
+
14
+ # Uses ImageMagick to determing the dimensions of a file, passed in as either a
15
+ # File or path.
16
+ # NOTE: (race cond) Do not reassign the 'file' variable inside this method as it is likely to be
17
+ # a Tempfile object, which would be eligible for file deletion when no longer referenced.
18
+ def self.from_file file
19
+ file_path = file.respond_to?(:path) ? file.path : file
20
+ raise(Paperclip::NotIdentifiedByImageMagickError.new("Cannot find the geometry of a file with a blank name")) if file_path.blank?
21
+ geometry = begin
22
+ Paperclip.run("identify", "-format %wx%h :file", :file => "#{file_path}[0]")
23
+ rescue Cocaine::ExitStatusError
24
+ ""
25
+ rescue Cocaine::CommandNotFoundError => e
26
+ raise Paperclip::CommandNotFoundError.new("Could not run the `identify` command. Please install ImageMagick.")
27
+ end
28
+ parse(geometry) ||
29
+ raise(NotIdentifiedByImageMagickError.new("#{file_path} is not recognized by the 'identify' command."))
30
+ end
31
+
32
+ # Parses a "WxH" formatted string, where W is the width and H is the height.
33
+ def self.parse string
34
+ if match = (string && string.match(/\b(\d*)x?(\d*)\b([\>\<\#\@\%^!])?/i))
35
+ Geometry.new(*match[1,3])
36
+ end
37
+ end
38
+
39
+ # True if the dimensions represent a square
40
+ def square?
41
+ height == width
42
+ end
43
+
44
+ # True if the dimensions represent a horizontal rectangle
45
+ def horizontal?
46
+ height < width
47
+ end
48
+
49
+ # True if the dimensions represent a vertical rectangle
50
+ def vertical?
51
+ height > width
52
+ end
53
+
54
+ # The aspect ratio of the dimensions.
55
+ def aspect
56
+ width / height
57
+ end
58
+
59
+ # Returns the larger of the two dimensions
60
+ def larger
61
+ [height, width].max
62
+ end
63
+
64
+ # Returns the smaller of the two dimensions
65
+ def smaller
66
+ [height, width].min
67
+ end
68
+
69
+ # Returns the width and height in a format suitable to be passed to Geometry.parse
70
+ def to_s
71
+ s = ""
72
+ s << width.to_i.to_s if width > 0
73
+ s << "x#{height.to_i}" if height > 0
74
+ s << modifier.to_s
75
+ s
76
+ end
77
+
78
+ # Same as to_s
79
+ def inspect
80
+ to_s
81
+ end
82
+
83
+ # Returns the scaling and cropping geometries (in string-based ImageMagick format)
84
+ # neccessary to transform this Geometry into the Geometry given. If crop is true,
85
+ # then it is assumed the destination Geometry will be the exact final resolution.
86
+ # In this case, the source Geometry is scaled so that an image containing the
87
+ # destination Geometry would be completely filled by the source image, and any
88
+ # overhanging image would be cropped. Useful for square thumbnail images. The cropping
89
+ # is weighted at the center of the Geometry.
90
+ def transformation_to dst, crop = false
91
+ if crop
92
+ ratio = Geometry.new( dst.width / self.width, dst.height / self.height )
93
+ scale_geometry, scale = scaling(dst, ratio)
94
+ crop_geometry = cropping(dst, ratio, scale)
95
+ else
96
+ scale_geometry = dst.to_s
97
+ end
98
+
99
+ [ scale_geometry, crop_geometry ]
100
+ end
101
+
102
+ private
103
+
104
+ def scaling dst, ratio
105
+ if ratio.horizontal? || ratio.square?
106
+ [ "%dx" % dst.width, ratio.width ]
107
+ else
108
+ [ "x%d" % dst.height, ratio.height ]
109
+ end
110
+ end
111
+
112
+ def cropping dst, ratio, scale
113
+ if ratio.horizontal? || ratio.square?
114
+ "%dx%d+%d+%d" % [ dst.width, dst.height, 0, (self.height * scale - dst.height) / 2 ]
115
+ else
116
+ "%dx%d+%d+%d" % [ dst.width, dst.height, (self.width * scale - dst.width) / 2, 0 ]
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,174 @@
1
+ module Paperclip
2
+ # This module contains all the methods that are available for interpolation
3
+ # in paths and urls. To add your own (or override an existing one), you
4
+ # can either open this module and define it, or call the
5
+ # Paperclip.interpolates method.
6
+ module Interpolations
7
+ extend self
8
+
9
+ # Hash assignment of interpolations. Included only for compatibility,
10
+ # and is not intended for normal use.
11
+ def self.[]= name, block
12
+ define_method(name, &block)
13
+ end
14
+
15
+ # Hash access of interpolations. Included only for compatibility,
16
+ # and is not intended for normal use.
17
+ def self.[] name
18
+ method(name)
19
+ end
20
+
21
+ # Returns a sorted list of all interpolations.
22
+ def self.all
23
+ self.instance_methods(false).sort
24
+ end
25
+
26
+ # Perform the actual interpolation. Takes the pattern to interpolate
27
+ # and the arguments to pass, which are the attachment and style name.
28
+ # You can pass a method name on your record as a symbol, which should turn
29
+ # an interpolation pattern for Paperclip to use.
30
+ def self.interpolate pattern, *args
31
+ pattern = args.first.instance.send(pattern) if pattern.kind_of? Symbol
32
+ all.reverse.inject(pattern) do |result, tag|
33
+ result.gsub(/:#{tag}/) do |match|
34
+ send( tag, *args )
35
+ end
36
+ end
37
+ end
38
+
39
+ # Returns the filename, the same way as ":basename.:extension" would.
40
+ def filename attachment, style_name
41
+ [ basename(attachment, style_name), extension(attachment, style_name) ].reject(&:blank?).join(".")
42
+ end
43
+
44
+ # Returns the interpolated URL. Will raise an error if the url itself
45
+ # contains ":url" to prevent infinite recursion. This interpolation
46
+ # is used in the default :path to ease default specifications.
47
+ RIGHT_HERE = "#{__FILE__.gsub(%r{^\./}, "")}:#{__LINE__ + 3}"
48
+ def url attachment, style_name
49
+ raise InfiniteInterpolationError if caller.any?{|b| b.index(RIGHT_HERE) }
50
+ attachment.url(style_name, :timestamp => false, :escape => false)
51
+ end
52
+
53
+ # Returns the timestamp as defined by the <attachment>_updated_at field
54
+ # in the server default time zone unless :use_global_time_zone is set
55
+ # to false. Note that a Rails.config.time_zone change will still
56
+ # invalidate any path or URL that uses :timestamp. For a
57
+ # time_zone-agnostic timestamp, use #updated_at.
58
+ def timestamp attachment, style_name
59
+ attachment.instance_read(:updated_at).in_time_zone(attachment.time_zone).to_s
60
+ end
61
+
62
+ # Returns an integer timestamp that is time zone-neutral, so that paths
63
+ # remain valid even if a server's time zone changes.
64
+ def updated_at attachment, style_name
65
+ attachment.updated_at
66
+ end
67
+
68
+ # Returns the Rails.root constant.
69
+ def rails_root attachment, style_name
70
+ Rails.root
71
+ end
72
+
73
+ # Returns the Rails.env constant.
74
+ def rails_env attachment, style_name
75
+ Rails.env
76
+ end
77
+
78
+ # Returns the underscored, pluralized version of the class name.
79
+ # e.g. "users" for the User class.
80
+ # NOTE: The arguments need to be optional, because some tools fetch
81
+ # all class names. Calling #class will return the expected class.
82
+ def class attachment = nil, style_name = nil
83
+ return super() if attachment.nil? && style_name.nil?
84
+ attachment.instance.class.to_s.underscore.pluralize
85
+ end
86
+
87
+ # Returns the basename of the file. e.g. "file" for "file.jpg"
88
+ def basename attachment, style_name
89
+ attachment.original_filename.gsub(/#{Regexp.escape(File.extname(attachment.original_filename))}$/, "")
90
+ end
91
+
92
+ # Returns the extension of the file. e.g. "jpg" for "file.jpg"
93
+ # If the style has a format defined, it will return the format instead
94
+ # of the actual extension.
95
+ def extension attachment, style_name
96
+ ((style = attachment.styles[style_name]) && style[:format]) ||
97
+ File.extname(attachment.original_filename).gsub(/^\.+/, "")
98
+ end
99
+
100
+ # Returns an extension based on the content type. e.g. "jpeg" for "image/jpeg".
101
+ # Each mime type generally has multiple extensions associated with it, so
102
+ # if the extension from teh original filename is one of these extensions,
103
+ # that extension is used, otherwise, the first in the list is used.
104
+ def content_type_extension attachment, style_name
105
+ mime_type = MIME::Types[attachment.content_type]
106
+ extensions_for_mime_type = unless mime_type.empty?
107
+ mime_type.first.extensions
108
+ else
109
+ []
110
+ end
111
+
112
+ original_extension = extension(attachment, style_name)
113
+ if extensions_for_mime_type.include? original_extension
114
+ original_extension
115
+ elsif !extensions_for_mime_type.empty?
116
+ extensions_for_mime_type.first
117
+ else
118
+ # It's possible, though unlikely, that the mime type is not in the
119
+ # database, so just use the part after the '/' in the mime type as the
120
+ # extension.
121
+ %r{/([^/]*)$}.match(attachment.content_type)[1]
122
+ end
123
+ end
124
+
125
+ # Returns the id of the instance.
126
+ def id attachment, style_name
127
+ attachment.instance.id
128
+ end
129
+
130
+ # Returns the #to_param of the instance.
131
+ def param attachment, style_name
132
+ attachment.instance.to_param
133
+ end
134
+
135
+ # Returns the fingerprint of the instance.
136
+ def fingerprint attachment, style_name
137
+ attachment.fingerprint
138
+ end
139
+
140
+ # Returns a the attachment hash. See Paperclip::Attachment#hash for
141
+ # more details.
142
+ def hash attachment=nil, style_name=nil
143
+ if attachment && style_name
144
+ attachment.hash(style_name)
145
+ else
146
+ super()
147
+ end
148
+ end
149
+
150
+ # Returns the id of the instance in a split path form. e.g. returns
151
+ # 000/001/234 for an id of 1234.
152
+ def id_partition attachment, style_name
153
+ case id = attachment.instance.id
154
+ when Integer
155
+ ("%09d" % id).scan(/\d{3}/).join("/")
156
+ when String
157
+ id.scan(/.{3}/).first(3).join("/")
158
+ else
159
+ nil
160
+ end
161
+ end
162
+
163
+ # Returns the pluralized form of the attachment name. e.g.
164
+ # "avatars" for an attachment of :avatar
165
+ def attachment attachment, style_name
166
+ attachment.name.to_s.downcase.pluralize
167
+ end
168
+
169
+ # Returns the style, or the default style if nil is supplied.
170
+ def style attachment, style_name
171
+ style_name || attachment.default_style
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,45 @@
1
+ # Provides method that can be included on File-type objects (IO, StringIO, Tempfile, etc) to allow stream copying
2
+ # and Tempfile conversion.
3
+ module IOStream
4
+ # Returns a Tempfile containing the contents of the readable object.
5
+ def to_tempfile(object)
6
+ return object.to_tempfile if object.respond_to?(:to_tempfile)
7
+ name = object.respond_to?(:original_filename) ? object.original_filename : (object.respond_to?(:path) ? object.path : "stream")
8
+ tempfile = Paperclip::Tempfile.new(["stream", File.extname(name)])
9
+ tempfile.binmode
10
+ stream_to(object, tempfile)
11
+ end
12
+
13
+ # Copies one read-able object from one place to another in blocks, obviating the need to load
14
+ # the whole thing into memory. Defaults to 8k blocks. Returns a File if a String is passed
15
+ # in as the destination and returns the IO or Tempfile as passed in if one is sent as the destination.
16
+ def stream_to object, path_or_file, in_blocks_of = 8192
17
+ dstio = case path_or_file
18
+ when String then File.new(path_or_file, "wb+")
19
+ when IO then path_or_file
20
+ when Tempfile then path_or_file
21
+ end
22
+ buffer = ""
23
+ object.rewind
24
+ while object.read(in_blocks_of, buffer) do
25
+ dstio.write(buffer)
26
+ end
27
+ dstio.rewind
28
+ dstio
29
+ end
30
+ end
31
+
32
+ # Corrects a bug in Windows when asking for Tempfile size.
33
+ if defined?(Tempfile) && RUBY_PLATFORM !~ /java/
34
+ class Tempfile
35
+ def size
36
+ if @tmpfile
37
+ @tmpfile.fsync
38
+ @tmpfile.flush
39
+ @tmpfile.stat.size
40
+ else
41
+ 0
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,57 @@
1
+ module Paperclip
2
+ module Shoulda
3
+ module Matchers
4
+ # Ensures that the given instance or class has an attachment with the
5
+ # given name.
6
+ #
7
+ # Example:
8
+ # describe User do
9
+ # it { should have_attached_file(:avatar) }
10
+ # end
11
+ def have_attached_file name
12
+ HaveAttachedFileMatcher.new(name)
13
+ end
14
+
15
+ class HaveAttachedFileMatcher
16
+ def initialize attachment_name
17
+ @attachment_name = attachment_name
18
+ end
19
+
20
+ def matches? subject
21
+ @subject = subject
22
+ @subject = @subject.class unless Class === @subject
23
+ responds? && has_column? && included?
24
+ end
25
+
26
+ def failure_message
27
+ "Should have an attachment named #{@attachment_name}"
28
+ end
29
+
30
+ def negative_failure_message
31
+ "Should not have an attachment named #{@attachment_name}"
32
+ end
33
+
34
+ def description
35
+ "have an attachment named #{@attachment_name}"
36
+ end
37
+
38
+ protected
39
+
40
+ def responds?
41
+ methods = @subject.instance_methods.map(&:to_s)
42
+ methods.include?("#{@attachment_name}") &&
43
+ methods.include?("#{@attachment_name}=") &&
44
+ methods.include?("#{@attachment_name}?")
45
+ end
46
+
47
+ def has_column?
48
+ @subject.column_names.include?("#{@attachment_name}_file_name")
49
+ end
50
+
51
+ def included?
52
+ @subject.ancestors.include?(Paperclip::InstanceMethods)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,81 @@
1
+ module Paperclip
2
+ module Shoulda
3
+ module Matchers
4
+ # Ensures that the given instance or class validates the content type of
5
+ # the given attachment as specified.
6
+ #
7
+ # Example:
8
+ # describe User do
9
+ # it { should validate_attachment_content_type(:icon).
10
+ # allowing('image/png', 'image/gif').
11
+ # rejecting('text/plain', 'text/xml') }
12
+ # end
13
+ def validate_attachment_content_type name
14
+ ValidateAttachmentContentTypeMatcher.new(name)
15
+ end
16
+
17
+ class ValidateAttachmentContentTypeMatcher
18
+ def initialize attachment_name
19
+ @attachment_name = attachment_name
20
+ @allowed_types = []
21
+ @rejected_types = []
22
+ end
23
+
24
+ def allowing *types
25
+ @allowed_types = types.flatten
26
+ self
27
+ end
28
+
29
+ def rejecting *types
30
+ @rejected_types = types.flatten
31
+ self
32
+ end
33
+
34
+ def matches? subject
35
+ @subject = subject
36
+ @subject = @subject.class unless Class === @subject
37
+ @allowed_types && @rejected_types &&
38
+ allowed_types_allowed? && rejected_types_rejected?
39
+ end
40
+
41
+ def failure_message
42
+ "".tap do |str|
43
+ str << "Content types #{@allowed_types.join(", ")} should be accepted" if @allowed_types.present?
44
+ str << "\n" if @allowed_types.present? && @rejected_types.present?
45
+ str << "Content types #{@rejected_types.join(", ")} should be rejected by #{@attachment_name}" if @rejected_types.present?
46
+ end
47
+ end
48
+
49
+ def negative_failure_message
50
+ "".tap do |str|
51
+ str << "Content types #{@allowed_types.join(", ")} should be rejected" if @allowed_types.present?
52
+ str << "\n" if @allowed_types.present? && @rejected_types.present?
53
+ str << "Content types #{@rejected_types.join(", ")} should be accepted by #{@attachment_name}" if @rejected_types.present?
54
+ end
55
+ end
56
+
57
+ def description
58
+ "validate the content types allowed on attachment #{@attachment_name}"
59
+ end
60
+
61
+ protected
62
+
63
+ def type_allowed?(type)
64
+ file = StringIO.new(".")
65
+ file.content_type = type
66
+ (subject = @subject.new).attachment_for(@attachment_name).assign(file)
67
+ subject.valid?
68
+ subject.errors[:"#{@attachment_name}_content_type"].blank?
69
+ end
70
+
71
+ def allowed_types_allowed?
72
+ @allowed_types.all? { |type| type_allowed?(type) }
73
+ end
74
+
75
+ def rejected_types_rejected?
76
+ !@rejected_types.any? { |type| type_allowed?(type) }
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,54 @@
1
+ module Paperclip
2
+ module Shoulda
3
+ module Matchers
4
+ # Ensures that the given instance or class validates the presence of the
5
+ # given attachment.
6
+ #
7
+ # describe User do
8
+ # it { should validate_attachment_presence(:avatar) }
9
+ # end
10
+ def validate_attachment_presence name
11
+ ValidateAttachmentPresenceMatcher.new(name)
12
+ end
13
+
14
+ class ValidateAttachmentPresenceMatcher
15
+ def initialize attachment_name
16
+ @attachment_name = attachment_name
17
+ end
18
+
19
+ def matches? subject
20
+ @subject = subject
21
+ @subject = @subject.class unless Class === @subject
22
+ error_when_not_valid? && no_error_when_valid?
23
+ end
24
+
25
+ def failure_message
26
+ "Attachment #{@attachment_name} should be required"
27
+ end
28
+
29
+ def negative_failure_message
30
+ "Attachment #{@attachment_name} should not be required"
31
+ end
32
+
33
+ def description
34
+ "require presence of attachment #{@attachment_name}"
35
+ end
36
+
37
+ protected
38
+
39
+ def error_when_not_valid?
40
+ (subject = @subject.new).send(@attachment_name).assign(nil)
41
+ subject.valid?
42
+ not subject.errors[:"#{@attachment_name}_file_name"].blank?
43
+ end
44
+
45
+ def no_error_when_valid?
46
+ @file = StringIO.new(".")
47
+ (subject = @subject.new).send(@attachment_name).assign(@file)
48
+ subject.valid?
49
+ subject.errors[:"#{@attachment_name}_file_name"].blank?
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,95 @@
1
+ module Paperclip
2
+ module Shoulda
3
+ module Matchers
4
+ # Ensures that the given instance or class validates the size of the
5
+ # given attachment as specified.
6
+ #
7
+ # Examples:
8
+ # it { should validate_attachment_size(:avatar).
9
+ # less_than(2.megabytes) }
10
+ # it { should validate_attachment_size(:icon).
11
+ # greater_than(1024) }
12
+ # it { should validate_attachment_size(:icon).
13
+ # in(0..100) }
14
+ def validate_attachment_size name
15
+ ValidateAttachmentSizeMatcher.new(name)
16
+ end
17
+
18
+ class ValidateAttachmentSizeMatcher
19
+ def initialize attachment_name
20
+ @attachment_name = attachment_name
21
+ @low, @high = 0, (1.0/0)
22
+ end
23
+
24
+ def less_than size
25
+ @high = size
26
+ self
27
+ end
28
+
29
+ def greater_than size
30
+ @low = size
31
+ self
32
+ end
33
+
34
+ def in range
35
+ @low, @high = range.first, range.last
36
+ self
37
+ end
38
+
39
+ def matches? subject
40
+ @subject = subject
41
+ @subject = @subject.class unless Class === @subject
42
+ lower_than_low? && higher_than_low? && lower_than_high? && higher_than_high?
43
+ end
44
+
45
+ def failure_message
46
+ "Attachment #{@attachment_name} must be between #{@low} and #{@high} bytes"
47
+ end
48
+
49
+ def negative_failure_message
50
+ "Attachment #{@attachment_name} cannot be between #{@low} and #{@high} bytes"
51
+ end
52
+
53
+ def description
54
+ "validate the size of attachment #{@attachment_name}"
55
+ end
56
+
57
+ protected
58
+
59
+ def override_method object, method, &replacement
60
+ (class << object; self; end).class_eval do
61
+ define_method(method, &replacement)
62
+ end
63
+ end
64
+
65
+ def passes_validation_with_size(new_size)
66
+ file = StringIO.new(".")
67
+ override_method(file, :size){ new_size }
68
+ override_method(file, :to_tempfile){ file }
69
+
70
+ (subject = @subject.new).send(@attachment_name).assign(file)
71
+ subject.valid?
72
+ subject.errors[:"#{@attachment_name}_file_size"].blank?
73
+ end
74
+
75
+ def lower_than_low?
76
+ not passes_validation_with_size(@low - 1)
77
+ end
78
+
79
+ def higher_than_low?
80
+ passes_validation_with_size(@low + 1)
81
+ end
82
+
83
+ def lower_than_high?
84
+ return true if @high == (1.0/0)
85
+ passes_validation_with_size(@high - 1)
86
+ end
87
+
88
+ def higher_than_high?
89
+ return true if @high == (1.0/0)
90
+ not passes_validation_with_size(@high + 1)
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,64 @@
1
+ require 'paperclip/matchers/have_attached_file_matcher'
2
+ require 'paperclip/matchers/validate_attachment_presence_matcher'
3
+ require 'paperclip/matchers/validate_attachment_content_type_matcher'
4
+ require 'paperclip/matchers/validate_attachment_size_matcher'
5
+
6
+ module Paperclip
7
+ module Shoulda
8
+ # Provides RSpec-compatible & Test::Unit-compatible matchers for testing Paperclip attachments.
9
+ #
10
+ # *RSpec*
11
+ #
12
+ # In spec_helper.rb, you'll need to require the matchers:
13
+ #
14
+ # require "paperclip/matchers"
15
+ #
16
+ # And _include_ the module:
17
+ #
18
+ # Spec::Runner.configure do |config|
19
+ # config.include Paperclip::Shoulda::Matchers
20
+ # end
21
+ #
22
+ # Example:
23
+ # describe User do
24
+ # it { should have_attached_file(:avatar) }
25
+ # it { should validate_attachment_presence(:avatar) }
26
+ # it { should validate_attachment_content_type(:avatar).
27
+ # allowing('image/png', 'image/gif').
28
+ # rejecting('text/plain', 'text/xml') }
29
+ # it { should validate_attachment_size(:avatar).
30
+ # less_than(2.megabytes) }
31
+ # end
32
+ #
33
+ #
34
+ # *TestUnit*
35
+ #
36
+ # In test_helper.rb, you'll need to require the matchers as well:
37
+ #
38
+ # require "paperclip/matchers"
39
+ #
40
+ # And _extend_ the module:
41
+ #
42
+ # class ActiveSupport::TestCase
43
+ # extend Paperclip::Shoulda::Matchers
44
+ #
45
+ # #...other initializers...#
46
+ # end
47
+ #
48
+ # Example:
49
+ # require 'test_helper'
50
+ #
51
+ # class UserTest < ActiveSupport::TestCase
52
+ # should have_attached_file(:avatar)
53
+ # should validate_attachment_presence(:avatar)
54
+ # should validate_attachment_content_type(:avatar).
55
+ # allowing('image/png', 'image/gif').
56
+ # rejecting('text/plain', 'text/xml')
57
+ # should validate_attachment_size(:avatar).
58
+ # less_than(2.megabytes)
59
+ # end
60
+ #
61
+ module Matchers
62
+ end
63
+ end
64
+ end