paperclip 2.3.1.1 → 2.3.2

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of paperclip might be problematic. Click here for more details.

Files changed (44) hide show
  1. data/README.rdoc +6 -1
  2. data/Rakefile +11 -38
  3. data/generators/paperclip/USAGE +2 -2
  4. data/generators/paperclip/paperclip_generator.rb +8 -8
  5. data/lib/generators/paperclip/USAGE +8 -0
  6. data/lib/generators/paperclip/paperclip_generator.rb +31 -0
  7. data/lib/generators/paperclip/templates/paperclip_migration.rb.erb +19 -0
  8. data/lib/paperclip.rb +113 -69
  9. data/lib/paperclip/attachment.rb +58 -146
  10. data/lib/paperclip/callback_compatability.rb +50 -22
  11. data/lib/paperclip/geometry.rb +7 -7
  12. data/lib/paperclip/interpolations.rb +21 -21
  13. data/lib/paperclip/iostream.rb +3 -2
  14. data/lib/paperclip/matchers.rb +29 -0
  15. data/lib/paperclip/matchers/have_attached_file_matcher.rb +8 -0
  16. data/lib/paperclip/matchers/validate_attachment_content_type_matcher.rb +13 -5
  17. data/lib/paperclip/matchers/validate_attachment_presence_matcher.rb +13 -7
  18. data/lib/paperclip/matchers/validate_attachment_size_matcher.rb +16 -4
  19. data/lib/paperclip/processor.rb +2 -2
  20. data/lib/paperclip/railtie.rb +22 -0
  21. data/lib/paperclip/storage.rb +29 -25
  22. data/lib/paperclip/style.rb +90 -0
  23. data/lib/paperclip/thumbnail.rb +20 -15
  24. data/lib/paperclip/upfile.rb +5 -2
  25. data/lib/paperclip/version.rb +3 -0
  26. data/{tasks/paperclip_tasks.rake → lib/tasks/paperclip.rake} +0 -0
  27. data/rails/init.rb +2 -0
  28. data/shoulda_macros/paperclip.rb +5 -3
  29. data/test/attachment_test.rb +52 -74
  30. data/test/geometry_test.rb +1 -1
  31. data/test/helper.rb +62 -22
  32. data/test/integration_test.rb +8 -8
  33. data/test/interpolations_test.rb +4 -4
  34. data/test/iostream_test.rb +9 -2
  35. data/test/matchers/have_attached_file_matcher_test.rb +9 -6
  36. data/test/matchers/validate_attachment_content_type_matcher_test.rb +15 -8
  37. data/test/matchers/validate_attachment_presence_matcher_test.rb +11 -6
  38. data/test/matchers/validate_attachment_size_matcher_test.rb +18 -17
  39. data/test/paperclip_test.rb +58 -68
  40. data/test/storage_test.rb +53 -13
  41. data/test/style_test.rb +141 -0
  42. data/test/thumbnail_test.rb +17 -17
  43. data/test/upfile_test.rb +8 -0
  44. metadata +69 -42
@@ -4,21 +4,22 @@ module Paperclip
4
4
  # when the model saves, deletes when the model is destroyed, and processes
5
5
  # the file upon assignment.
6
6
  class Attachment
7
-
7
+
8
8
  def self.default_options
9
9
  @default_options ||= {
10
- :url => "/system/:attachment/:id/:style/:filename",
11
- :path => ":rails_root/public:url",
12
- :styles => {},
13
- :default_url => "/:attachment/:style/missing.png",
14
- :default_style => :original,
15
- :validations => [],
16
- :storage => :filesystem,
17
- :whiny => Paperclip.options[:whiny] || Paperclip.options[:whiny_thumbnails]
10
+ :url => "/system/:attachment/:id/:style/:filename",
11
+ :path => ":rails_root/public:url",
12
+ :styles => {},
13
+ :processors => [:thumbnail],
14
+ :convert_options => {},
15
+ :default_url => "/:attachment/:style/missing.png",
16
+ :default_style => :original,
17
+ :storage => :filesystem,
18
+ :whiny => Paperclip.options[:whiny] || Paperclip.options[:whiny_thumbnails]
18
19
  }
19
20
  end
20
21
 
21
- attr_reader :name, :instance, :styles, :default_style, :convert_options, :queued_for_write, :options
22
+ attr_reader :name, :instance, :default_style, :convert_options, :queued_for_write, :whiny, :options
22
23
 
23
24
  # Creates an Attachment object. +name+ is the name of the attachment,
24
25
  # +instance+ is the ActiveRecord object instance it's attached to, and
@@ -34,33 +35,42 @@ module Paperclip
34
35
  @path = options[:path]
35
36
  @path = @path.call(self) if @path.is_a?(Proc)
36
37
  @styles = options[:styles]
37
- @styles = @styles.call(self) if @styles.is_a?(Proc)
38
+ @normalized_styles = nil
38
39
  @default_url = options[:default_url]
39
- @validations = options[:validations]
40
40
  @default_style = options[:default_style]
41
41
  @storage = options[:storage]
42
42
  @whiny = options[:whiny_thumbnails] || options[:whiny]
43
- @convert_options = options[:convert_options] || {}
44
- @processors = options[:processors] || [:thumbnail]
43
+ @convert_options = options[:convert_options]
44
+ @processors = options[:processors]
45
45
  @options = options
46
46
  @queued_for_delete = []
47
47
  @queued_for_write = {}
48
48
  @errors = {}
49
- @validation_errors = nil
50
49
  @dirty = false
51
50
 
52
- normalize_style_definition
53
51
  initialize_storage
54
52
  end
55
53
 
54
+ def styles
55
+ unless @normalized_styles
56
+ @normalized_styles = {}
57
+ (@styles.respond_to?(:call) ? @styles.call(self) : @styles).each do |name, args|
58
+ @normalized_styles[name] = Paperclip::Style.new(name, args, self)
59
+ end
60
+ end
61
+ @normalized_styles
62
+ end
63
+
64
+ def processors
65
+ @processors.respond_to?(:call) ? @processors.call(instance) : @processors
66
+ end
67
+
56
68
  # What gets called when you call instance.attachment = File. It clears
57
- # errors, assigns attributes, processes the file, and runs validations. It
69
+ # errors, assigns attributes, and processes the file. It
58
70
  # also queues up the previous file for deletion, to be flushed away on
59
71
  # #save of its host. In addition to form uploads, you can also assign
60
- # another Paperclip attachment:
72
+ # another Paperclip attachment:
61
73
  # new_user.avatar = old_user.avatar
62
- # If the file that is assigned is not valid, the processing (i.e.
63
- # thumbnailing, etc) will NOT be run.
64
74
  def assign uploaded_file
65
75
  ensure_required_accessors!
66
76
 
@@ -77,20 +87,19 @@ module Paperclip
77
87
  return nil if uploaded_file.nil?
78
88
 
79
89
  @queued_for_write[:original] = uploaded_file.to_tempfile
80
- instance_write(:file_name, uploaded_file.original_filename.strip.gsub(/[^A-Za-z\d\.\-_]+/, '_'))
90
+ instance_write(:file_name, uploaded_file.original_filename.strip)
81
91
  instance_write(:content_type, uploaded_file.content_type.to_s.strip)
82
92
  instance_write(:file_size, uploaded_file.size.to_i)
83
93
  instance_write(:updated_at, Time.now)
84
94
 
85
95
  @dirty = true
86
96
 
87
- post_process if valid?
88
-
97
+ post_process
98
+
89
99
  # Reset the file size if the original file was reprocessed.
90
100
  instance_write(:file_size, @queued_for_write[:original].size.to_i)
91
101
  ensure
92
102
  uploaded_file.close if close_uploaded_file
93
- validate
94
103
  end
95
104
 
96
105
  # Returns the public URL of the attachment, with a given style. Note that
@@ -100,8 +109,8 @@ module Paperclip
100
109
  # security, however, for performance reasons. set
101
110
  # include_updated_timestamp to false if you want to stop the attachment
102
111
  # update time appended to the url
103
- def url style = default_style, include_updated_timestamp = true
104
- url = original_filename.nil? ? interpolate(@default_url, style) : interpolate(@url, style)
112
+ def url style_name = default_style, include_updated_timestamp = true
113
+ url = original_filename.nil? ? interpolate(@default_url, style_name) : interpolate(@url, style_name)
105
114
  include_updated_timestamp && updated_at ? [url, updated_at].compact.join(url.include?("?") ? "&" : "?") : url
106
115
  end
107
116
 
@@ -109,19 +118,13 @@ module Paperclip
109
118
  # file is stored in the filesystem the path refers to the path of the file
110
119
  # on disk. If the file is stored in S3, the path is the "key" part of the
111
120
  # URL, and the :bucket option refers to the S3 bucket.
112
- def path style = default_style
113
- original_filename.nil? ? nil : interpolate(@path, style)
121
+ def path style_name = default_style
122
+ original_filename.nil? ? nil : interpolate(@path, style_name)
114
123
  end
115
124
 
116
125
  # Alias to +url+
117
- def to_s style = nil
118
- url(style)
119
- end
120
-
121
- # Returns true if there are no errors on this attachment.
122
- def valid?
123
- validate
124
- errors.empty?
126
+ def to_s style_name = nil
127
+ url(style_name)
125
128
  end
126
129
 
127
130
  # Returns an array containing the errors on this attachment.
@@ -137,15 +140,10 @@ module Paperclip
137
140
  # Saves the file, if there are no errors. If there are, it flushes them to
138
141
  # the instance's errors and returns false, cancelling the save.
139
142
  def save
140
- if valid?
141
- flush_deletes
142
- flush_writes
143
- @dirty = false
144
- true
145
- else
146
- flush_errors
147
- false
148
- end
143
+ flush_deletes
144
+ flush_writes
145
+ @dirty = false
146
+ true
149
147
  end
150
148
 
151
149
  # Clears out the attachment. Has the same effect as previously assigning
@@ -154,7 +152,6 @@ module Paperclip
154
152
  def clear
155
153
  queue_existing_for_delete
156
154
  @errors = {}
157
- @validation_errors = nil
158
155
  end
159
156
 
160
157
  # Destroys the attachment. Has the same effect as previously assigning
@@ -182,8 +179,8 @@ module Paperclip
182
179
  def content_type
183
180
  instance_read(:content_type)
184
181
  end
185
-
186
- # Returns the last modified time of the file as originally assigned, and
182
+
183
+ # Returns the last modified time of the file as originally assigned, and
187
184
  # lives in the <attachment>_updated_at attribute of the model.
188
185
  def updated_at
189
186
  time = instance_read(:updated_at)
@@ -223,7 +220,7 @@ module Paperclip
223
220
  true
224
221
  end
225
222
  end
226
-
223
+
227
224
  # Returns true if a file has been assigned.
228
225
  def file?
229
226
  !original_filename.blank?
@@ -267,82 +264,6 @@ module Paperclip
267
264
  file.nil? || (file.respond_to?(:original_filename) && file.respond_to?(:content_type))
268
265
  end
269
266
 
270
- def validate #:nodoc:
271
- unless @validation_errors
272
- @validation_errors = @validations.inject({}) do |errors, validation|
273
- name, options = validation
274
- errors[name] = send(:"validate_#{name}", options) if allow_validation?(options)
275
- errors
276
- end
277
- @validation_errors.reject!{|k,v| v == nil }
278
- @errors.merge!(@validation_errors)
279
- end
280
- @validation_errors
281
- end
282
-
283
- def allow_validation? options #:nodoc:
284
- (options[:if].nil? || check_guard(options[:if])) && (options[:unless].nil? || !check_guard(options[:unless]))
285
- end
286
-
287
- def check_guard guard #:nodoc:
288
- if guard.respond_to? :call
289
- guard.call(instance)
290
- elsif ! guard.blank?
291
- instance.send(guard.to_s)
292
- end
293
- end
294
-
295
- def validate_size options #:nodoc:
296
- if file? && !options[:range].include?(size.to_i)
297
- options[:message].gsub(/:min/, options[:min].to_s).gsub(/:max/, options[:max].to_s)
298
- end
299
- end
300
-
301
- def validate_presence options #:nodoc:
302
- options[:message] unless file?
303
- end
304
-
305
- def validate_content_type options #:nodoc:
306
- valid_types = [options[:content_type]].flatten
307
- unless original_filename.blank?
308
- unless valid_types.blank?
309
- content_type = instance_read(:content_type)
310
- unless valid_types.any?{|t| content_type.nil? || t === content_type }
311
- options[:message] || "is not one of the allowed file types."
312
- end
313
- end
314
- end
315
- end
316
-
317
- def normalize_style_definition #:nodoc:
318
- @styles.each do |name, args|
319
- unless args.is_a? Hash
320
- dimensions, format = [args, nil].flatten[0..1]
321
- format = nil if format.blank?
322
- @styles[name] = {
323
- :processors => @processors,
324
- :geometry => dimensions,
325
- :format => format,
326
- :whiny => @whiny,
327
- :convert_options => extra_options_for(name)
328
- }
329
- else
330
- @styles[name] = {
331
- :processors => @processors,
332
- :whiny => @whiny,
333
- :convert_options => extra_options_for(name)
334
- }.merge(@styles[name])
335
- end
336
- end
337
- end
338
-
339
- def solidify_style_definitions #:nodoc:
340
- @styles.each do |name, args|
341
- @styles[name][:geometry] = @styles[name][:geometry].call(instance) if @styles[name][:geometry].respond_to?(:call)
342
- @styles[name][:processors] = @styles[name][:processors].call(instance) if @styles[name][:processors].respond_to?(:call)
343
- end
344
- end
345
-
346
267
  def initialize_storage #:nodoc:
347
268
  @storage_module = Paperclip::Storage.const_get(@storage.to_s.capitalize)
348
269
  self.extend(@storage_module)
@@ -359,27 +280,19 @@ module Paperclip
359
280
 
360
281
  def post_process #:nodoc:
361
282
  return if @queued_for_write[:original].nil?
362
- solidify_style_definitions
363
- return if fire_events(:before)
364
- post_process_styles
365
- return if fire_events(:after)
366
- end
367
-
368
- def fire_events(which) #:nodoc:
369
- return true if callback(:"#{which}_post_process") == false
370
- return true if callback(:"#{which}_#{name}_post_process") == false
371
- end
372
-
373
- def callback which #:nodoc:
374
- instance.run_callbacks(which, @queued_for_write){|result, obj| result == false }
283
+ instance.run_paperclip_callbacks(:post_process) do
284
+ instance.run_paperclip_callbacks(:"#{name}_post_process") do
285
+ post_process_styles
286
+ end
287
+ end
375
288
  end
376
289
 
377
290
  def post_process_styles #:nodoc:
378
- @styles.each do |name, args|
291
+ styles.each do |name, style|
379
292
  begin
380
- raise RuntimeError.new("Style #{name} has no processors defined.") if args[:processors].blank?
381
- @queued_for_write[name] = args[:processors].inject(@queued_for_write[:original]) do |file, processor|
382
- Paperclip.processor(processor).make(file, args, self)
293
+ raise RuntimeError.new("Style #{name} has no processors defined.") if style.processors.blank?
294
+ @queued_for_write[name] = style.processors.inject(@queued_for_write[:original]) do |file, processor|
295
+ Paperclip.processor(processor).make(file, style.processor_options, self)
383
296
  end
384
297
  rescue PaperclipError => e
385
298
  log("An error was received while processing: #{e.inspect}")
@@ -388,13 +301,13 @@ module Paperclip
388
301
  end
389
302
  end
390
303
 
391
- def interpolate pattern, style = default_style #:nodoc:
392
- Paperclip::Interpolations.interpolate(pattern, self, style)
304
+ def interpolate pattern, style_name = default_style #:nodoc:
305
+ Paperclip::Interpolations.interpolate(pattern, self, style_name)
393
306
  end
394
307
 
395
308
  def queue_existing_for_delete #:nodoc:
396
309
  return unless file?
397
- @queued_for_delete += [:original, *@styles.keys].uniq.map do |style|
310
+ @queued_for_delete += [:original, *styles.keys].uniq.map do |style|
398
311
  path(style) if exists?(style)
399
312
  end.compact
400
313
  instance_write(:file_name, nil)
@@ -411,4 +324,3 @@ module Paperclip
411
324
 
412
325
  end
413
326
  end
414
-
@@ -1,33 +1,61 @@
1
1
  module Paperclip
2
- # This module is intended as a compatability shim for the differences in
3
- # callbacks between Rails 2.0 and Rails 2.1.
4
2
  module CallbackCompatability
5
- def self.included(base)
6
- base.extend(ClassMethods)
7
- base.send(:include, InstanceMethods)
8
- end
3
+ module Rails21
4
+ def self.included(base)
5
+ base.extend(Defining)
6
+ base.send(:include, Running)
7
+ end
9
8
 
10
- module ClassMethods
11
- # The implementation of this method is taken from the Rails 1.2.6 source,
12
- # from rails/activerecord/lib/active_record/callbacks.rb, line 192.
13
- def define_callbacks(*args)
14
- args.each do |method|
15
- self.class_eval <<-"end_eval"
16
- def self.#{method}(*callbacks, &block)
17
- callbacks << block if block_given?
18
- write_inheritable_array(#{method.to_sym.inspect}, callbacks)
19
- end
20
- end_eval
9
+ module Defining
10
+ def define_paperclip_callbacks(*args)
11
+ args.each do |callback|
12
+ define_callbacks("before_#{callback}")
13
+ define_callbacks("after_#{callback}")
14
+ end
15
+ end
16
+ end
17
+
18
+ module Running
19
+ def run_paperclip_callbacks(callback, opts = nil, &blk)
20
+ # The overall structure of this isn't ideal since after callbacks run even if
21
+ # befores return false. But this is how rails 3's callbacks work, unfortunately.
22
+ if run_callbacks(:"before_#{callback}"){ |result, object| result == false } != false
23
+ blk.call
24
+ end
25
+ run_callbacks(:"after_#{callback}"){ |result, object| result == false }
21
26
  end
22
27
  end
23
28
  end
24
29
 
25
- module InstanceMethods
26
- # The callbacks in < 2.1 don't worry about the extra options or the
27
- # block, so just run what we have available.
28
- def run_callbacks(meth, opts = nil, &blk)
29
- callback(meth)
30
+ module Rails3
31
+ def self.included(base)
32
+ base.extend(Defining)
33
+ base.send(:include, Running)
30
34
  end
35
+
36
+ module Defining
37
+ def define_paperclip_callbacks(*callbacks)
38
+ define_callbacks *[callbacks, {:terminator => "result == false"}].flatten
39
+ callbacks.each do |callback|
40
+ eval <<-end_callbacks
41
+ def before_#{callback}(*args, &blk)
42
+ set_callback(:#{callback}, :before, *args, &blk)
43
+ end
44
+ def after_#{callback}(*args, &blk)
45
+ set_callback(:#{callback}, :after, *args, &blk)
46
+ end
47
+ end_callbacks
48
+ end
49
+ end
50
+ end
51
+
52
+ module Running
53
+ def run_paperclip_callbacks(callback, opts = nil, &block)
54
+ run_callbacks(callback, opts, &block)
55
+ end
56
+ end
57
+
31
58
  end
59
+
32
60
  end
33
61
  end
@@ -16,7 +16,7 @@ module Paperclip
16
16
  def self.from_file file
17
17
  file = file.path if file.respond_to? "path"
18
18
  geometry = begin
19
- Paperclip.run("identify", %Q[-format "%wx%h" "#{file}"[0]])
19
+ Paperclip.run("identify", "-format", "%wx%h", "#{file}[0]")
20
20
  rescue PaperclipCommandLineError
21
21
  ""
22
22
  end
@@ -75,12 +75,12 @@ module Paperclip
75
75
  to_s
76
76
  end
77
77
 
78
- # Returns the scaling and cropping geometries (in string-based ImageMagick format)
79
- # neccessary to transform this Geometry into the Geometry given. If crop is true,
80
- # then it is assumed the destination Geometry will be the exact final resolution.
81
- # In this case, the source Geometry is scaled so that an image containing the
82
- # destination Geometry would be completely filled by the source image, and any
83
- # overhanging image would be cropped. Useful for square thumbnail images. The cropping
78
+ # Returns the scaling and cropping geometries (in string-based ImageMagick format)
79
+ # neccessary to transform this Geometry into the Geometry given. If crop is true,
80
+ # then it is assumed the destination Geometry will be the exact final resolution.
81
+ # In this case, the source Geometry is scaled so that an image containing the
82
+ # destination Geometry would be completely filled by the source image, and any
83
+ # overhanging image would be cropped. Useful for square thumbnail images. The cropping
84
84
  # is weighted at the center of the Geometry.
85
85
  def transformation_to dst, crop = false
86
86
  if crop
@@ -34,75 +34,75 @@ module Paperclip
34
34
  end
35
35
 
36
36
  # Returns the filename, the same way as ":basename.:extension" would.
37
- def filename attachment, style
38
- "#{basename(attachment, style)}.#{extension(attachment, style)}"
37
+ def filename attachment, style_name
38
+ "#{basename(attachment, style_name)}.#{extension(attachment, style_name)}"
39
39
  end
40
40
 
41
41
  # Returns the interpolated URL. Will raise an error if the url itself
42
42
  # contains ":url" to prevent infinite recursion. This interpolation
43
43
  # is used in the default :path to ease default specifications.
44
- def url attachment, style
44
+ def url attachment, style_name
45
45
  raise InfiniteInterpolationError if attachment.options[:url].include?(":url")
46
- attachment.url(style, false)
46
+ attachment.url(style_name, false)
47
47
  end
48
48
 
49
49
  # Returns the timestamp as defined by the <attachment>_updated_at field
50
- def timestamp attachment, style
50
+ def timestamp attachment, style_name
51
51
  attachment.instance_read(:updated_at).to_s
52
52
  end
53
53
 
54
- # Returns the RAILS_ROOT constant.
55
- def rails_root attachment, style
56
- RAILS_ROOT
54
+ # Returns the Rails.root constant.
55
+ def rails_root attachment, style_name
56
+ Rails.root
57
57
  end
58
58
 
59
- # Returns the RAILS_ENV constant.
60
- def rails_env attachment, style
61
- RAILS_ENV
59
+ # Returns the Rails.env constant.
60
+ def rails_env attachment, style_name
61
+ Rails.env
62
62
  end
63
63
 
64
64
  # Returns the underscored, pluralized version of the class name.
65
65
  # e.g. "users" for the User class.
66
66
  # NOTE: The arguments need to be optional, because some tools fetch
67
67
  # all class names. Calling #class will return the expected class.
68
- def class attachment = nil, style = nil
69
- return super() if attachment.nil? && style.nil?
68
+ def class attachment = nil, style_name = nil
69
+ return super() if attachment.nil? && style_name.nil?
70
70
  attachment.instance.class.to_s.underscore.pluralize
71
71
  end
72
72
 
73
73
  # Returns the basename of the file. e.g. "file" for "file.jpg"
74
- def basename attachment, style
74
+ def basename attachment, style_name
75
75
  attachment.original_filename.gsub(/#{File.extname(attachment.original_filename)}$/, "")
76
76
  end
77
77
 
78
78
  # Returns the extension of the file. e.g. "jpg" for "file.jpg"
79
79
  # If the style has a format defined, it will return the format instead
80
80
  # of the actual extension.
81
- def extension attachment, style
82
- ((style = attachment.styles[style]) && style[:format]) ||
81
+ def extension attachment, style_name
82
+ ((style = attachment.styles[style_name]) && style[:format]) ||
83
83
  File.extname(attachment.original_filename).gsub(/^\.+/, "")
84
84
  end
85
85
 
86
86
  # Returns the id of the instance.
87
- def id attachment, style
87
+ def id attachment, style_name
88
88
  attachment.instance.id
89
89
  end
90
90
 
91
91
  # Returns the id of the instance in a split path form. e.g. returns
92
92
  # 000/001/234 for an id of 1234.
93
- def id_partition attachment, style
93
+ def id_partition attachment, style_name
94
94
  ("%09d" % attachment.instance.id).scan(/\d{3}/).join("/")
95
95
  end
96
96
 
97
97
  # Returns the pluralized form of the attachment name. e.g.
98
98
  # "avatars" for an attachment of :avatar
99
- def attachment attachment, style
99
+ def attachment attachment, style_name
100
100
  attachment.name.to_s.downcase.pluralize
101
101
  end
102
102
 
103
103
  # Returns the style, or the default style if nil is supplied.
104
- def style attachment, style
105
- style || attachment.default_style
104
+ def style attachment, style_name
105
+ style_name || attachment.default_style
106
106
  end
107
107
  end
108
108
  end