avatar 0.0.4 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/History.txt +10 -0
  2. data/Manifest.txt +32 -6
  3. data/lib/avatar.rb +1 -1
  4. data/lib/avatar/source/gravatar_source.rb +7 -5
  5. data/lib/avatar/source/paperclip_source.rb +52 -0
  6. data/lib/avatar/source/wrapper.rb +6 -0
  7. data/lib/avatar/source/wrapper/abstract_source_wrapper.rb +33 -0
  8. data/lib/avatar/source/wrapper/rails_asset_source_wrapper.rb +36 -0
  9. data/lib/avatar/source/wrapper/string_substitution_source_wrapper.rb +55 -0
  10. data/lib/avatar/version.rb +1 -1
  11. data/test/lib/paperclip/README +32 -0
  12. data/test/lib/paperclip/Rakefile +41 -0
  13. data/test/lib/paperclip/generators/paperclip/USAGE +5 -0
  14. data/test/lib/paperclip/generators/paperclip/paperclip_generator.rb +27 -0
  15. data/test/lib/paperclip/generators/paperclip/templates/paperclip_migration.rb +17 -0
  16. data/test/lib/paperclip/init.rb +3 -0
  17. data/test/lib/paperclip/lib/paperclip.rb +197 -0
  18. data/test/lib/paperclip/lib/paperclip/attachment.rb +230 -0
  19. data/test/lib/paperclip/lib/paperclip/geometry.rb +109 -0
  20. data/test/lib/paperclip/lib/paperclip/iostream.rb +43 -0
  21. data/test/lib/paperclip/lib/paperclip/thumbnail.rb +80 -0
  22. data/test/lib/paperclip/lib/paperclip/upfile.rb +32 -0
  23. data/test/lib/paperclip/tasks/paperclip_tasks.rake +38 -0
  24. data/test/lib/paperclip/test/database.yml +5 -0
  25. data/test/lib/paperclip/test/fixtures/12k.png +0 -0
  26. data/test/lib/paperclip/test/fixtures/5k.png +0 -0
  27. data/test/lib/paperclip/test/fixtures/bad.png +1 -0
  28. data/test/lib/paperclip/test/helper.rb +38 -0
  29. data/test/lib/paperclip/test/test_attachment.rb +99 -0
  30. data/test/lib/paperclip/test/test_geometry.rb +142 -0
  31. data/test/lib/paperclip/test/test_integration.rb +76 -0
  32. data/test/lib/paperclip/test/test_iostream.rb +60 -0
  33. data/test/lib/paperclip/test/test_paperclip.rb +83 -0
  34. data/test/lib/paperclip/test/test_thumbnail.rb +76 -0
  35. data/test/lib/schema.rb +14 -1
  36. data/test/test_file_column_source.rb +8 -6
  37. data/test/test_gravatar_source.rb +5 -0
  38. data/test/test_helper.rb +7 -2
  39. data/test/test_paperclip_source.rb +52 -0
  40. data/test/test_rails_asset_source_wrapper.rb +37 -0
  41. data/test/test_string_substitution_source_wrapper.rb +25 -0
  42. data/website/index.html +1 -1
  43. metadata +43 -11
  44. data/lib/avatar/source/rails_asset_source.rb +0 -64
  45. data/lib/avatar/source/string_substitution_source.rb +0 -45
  46. data/lib/avatar/string_substitution.rb +0 -28
  47. data/test/test_rails_asset_source.rb +0 -50
  48. data/test/test_string_substitution.rb +0 -28
  49. data/test/test_string_substitution_source.rb +0 -22
@@ -0,0 +1,197 @@
1
+ # Paperclip allows file attachments that are stored in the filesystem. All graphical
2
+ # transformations are done using the Graphics/ImageMagick command line utilities and
3
+ # are stored in Tempfiles until the record is saved. Paperclip does not require a
4
+ # separate model for storing the attachment's information, instead adding a few simple
5
+ # columns to your table.
6
+ #
7
+ # Author:: Jon Yurek
8
+ # Copyright:: Copyright (c) 2008 thoughtbot, inc.
9
+ # License:: Distrbutes under the same terms as Ruby
10
+ #
11
+ # Paperclip defines an attachment as any file, though it makes special considerations
12
+ # for image files. You can declare that a model has an attached file with the
13
+ # +has_attached_file+ method:
14
+ #
15
+ # class User < ActiveRecord::Base
16
+ # has_attached_file :avatar, :styles => { :thumb => "100x100" }
17
+ # end
18
+ #
19
+ # user = User.new
20
+ # user.avatar = params[:user][:avatar]
21
+ # user.avatar.url
22
+ # # => "/users/avatars/4/original_me.jpg"
23
+ # user.avatar.url(:thumb)
24
+ # # => "/users/avatars/4/thumb_me.jpg"
25
+ #
26
+ # See the +has_attached_file+ documentation for more details.
27
+
28
+ require 'tempfile'
29
+ require 'paperclip/upfile'
30
+ require 'paperclip/iostream'
31
+ require 'paperclip/geometry'
32
+ require 'paperclip/thumbnail'
33
+ require 'paperclip/attachment'
34
+
35
+ # The base module that gets included in ActiveRecord::Base.
36
+ module Paperclip
37
+ class << self
38
+ # Provides configurability to Paperclip. There are a number of options available, such as:
39
+ # * whiny_thumbnails: Will raise an error if Paperclip cannot process thumbnails of
40
+ # an uploaded image. Defaults to true.
41
+ # * image_magick_path: Defines the path at which to find the +convert+ and +identify+
42
+ # programs if they are not visible to Rails the system's search path. Defaults to
43
+ # nil, which uses the first executable found in the search path.
44
+ def options
45
+ @options ||= {
46
+ :whiny_thumbnails => true,
47
+ :image_magick_path => nil
48
+ }
49
+ end
50
+
51
+ def path_for_command command #:nodoc:
52
+ path = [options[:image_magick_path], command].compact
53
+ File.join(*path)
54
+ end
55
+
56
+ def included base #:nodoc:
57
+ base.extend ClassMethods
58
+ end
59
+ end
60
+
61
+ class PaperclipError < StandardError #:nodoc:
62
+ end
63
+
64
+ module ClassMethods
65
+ attr_reader :attachment_definitions
66
+
67
+ # +has_attached_file+ gives the class it is called on an attribute that maps to a file. This
68
+ # is typically a file stored somewhere on the filesystem and has been uploaded by a user.
69
+ # The attribute returns a Paperclip::Attachment object which handles the management of
70
+ # that file. The intent is to make the attachment as much like a normal attribute. The
71
+ # thumbnails will be created when the new file is assigned, but they will *not* be saved
72
+ # until +save+ is called on the record. Likewise, if the attribute is set to +nil+ is
73
+ # called on it, the attachment will *not* be deleted until +save+ is called. See the
74
+ # Paperclip::Attachment documentation for more specifics. There are a number of options
75
+ # you can set to change the behavior of a Paperclip attachment:
76
+ # * +url+: The full URL of where the attachment is publically accessible. This can just
77
+ # as easily point to a directory served directly through Apache as it can to an action
78
+ # that can control permissions. You can specify the full domain and path, but usually
79
+ # just an absolute path is sufficient. The leading slash must be included manually for
80
+ # absolute paths. The default value is "/:class/:attachment/:id/:style_:filename". See
81
+ # Paperclip::Attachment#interpolate for more information on variable interpolaton.
82
+ # :url => "/:attachment/:id/:style_:basename:extension"
83
+ # :url => "http://some.other.host/stuff/:class/:id_:extension"
84
+ # * +default_url+: The URL that will be returned if there is no attachment assigned.
85
+ # This field is interpolated just as the url is. The default value is
86
+ # "/:class/:attachment/missing_:style.png"
87
+ # has_attached_file :avatar, :default_url => "/images/default_:style_avatar.png"
88
+ # User.new.avatar_url(:small) # => "/images/default_small_avatar.png"
89
+ # * +styles+: A hash of thumbnail styles and their geometries. You can find more about
90
+ # geometry strings at the ImageMagick website
91
+ # (http://www.imagemagick.org/script/command-line-options.php#resize). Paperclip
92
+ # also adds the "#" option (e.g. "50x50#"), which will resize the image to fit maximally
93
+ # inside the dimensions and then crop the rest off (weighted at the center). The
94
+ # default value is to generate no thumbnails.
95
+ # * +default_style+: The thumbnail style that will be used by default URLs.
96
+ # Defaults to +original+.
97
+ # has_attached_file :avatar, :styles => { :normal => "100x100#" },
98
+ # :default_style => :normal
99
+ # user.avatar.url # => "/avatars/23/normal_me.png"
100
+ # * +path+: The location of the repository of attachments on disk. This can be coordinated
101
+ # with the value of the +url+ option to allow files to be saved into a place where Apache
102
+ # can serve them without hitting your app. Defaults to
103
+ # ":rails_root/public/:class/:attachment/:id/:style_:filename".
104
+ # By default this places the files in the app's public directory which can be served
105
+ # directly. If you are using capistrano for deployment, a good idea would be to
106
+ # make a symlink to the capistrano-created system directory from inside your app's
107
+ # public directory.
108
+ # See Paperclip::Attachment#interpolate for more information on variable interpolaton.
109
+ # :path => "/var/app/attachments/:class/:id/:style/:filename"
110
+ # * +whiny_thumbnails+: Will raise an error if Paperclip cannot process thumbnails of an
111
+ # uploaded image. This will ovrride the global setting for this attachment.
112
+ # Defaults to true.
113
+ def has_attached_file name, options = {}
114
+ include InstanceMethods
115
+
116
+ @attachment_definitions ||= {}
117
+ @attachment_definitions[name] = {:validations => []}.merge(options)
118
+
119
+ after_save :save_attached_files
120
+ before_destroy :destroy_attached_files
121
+
122
+ define_method name do |*args|
123
+ a = attachment_for(name)
124
+ (args.length > 0) ? a.to_s(args.first) : a
125
+ end
126
+
127
+ define_method "#{name}=" do |file|
128
+ attachment_for(name).assign(file)
129
+ end
130
+
131
+ define_method "#{name}?" do
132
+ ! attachment_for(name).file.nil?
133
+ end
134
+
135
+ validates_each(name) do |record, attr, value|
136
+ value.send(:flush_errors)
137
+ end
138
+ end
139
+
140
+ # Places ActiveRecord-style validations on the size of the file assigned. The
141
+ # possible options are:
142
+ # * +in+: a Range of bytes (i.e. +1..1.megabyte+),
143
+ # * +less_than+: equivalent to :in => 0..options[:less_than]
144
+ # * +greater_than+: equivalent to :in => options[:greater_than]..Infinity
145
+ def validates_attachment_size name, options = {}
146
+ @attachment_definitions[name][:validations] << lambda do |attachment, instance|
147
+ unless options[:greater_than].nil?
148
+ options[:in] = (options[:greater_than]..(1/0)) # 1/0 => Infinity
149
+ end
150
+ unless options[:less_than].nil?
151
+ options[:in] = (0..options[:less_than])
152
+ end
153
+ unless options[:in].include? instance[:"#{name}_file_size"].to_i
154
+ "file size is not between #{options[:in].first} and #{options[:in].last} bytes."
155
+ end
156
+ end
157
+ end
158
+
159
+ # Places ActiveRecord-style validations on the presence of a file.
160
+ def validates_attachment_presence name
161
+ @attachment_definitions[name][:validations] << lambda do |attachment, instance|
162
+ if attachment.file.nil? || !File.exist?(attachment.file.path)
163
+ "must be set."
164
+ end
165
+ end
166
+ end
167
+
168
+ end
169
+
170
+ module InstanceMethods #:nodoc:
171
+ def attachment_for name
172
+ @attachments ||= {}
173
+ @attachments[name] ||= Attachment.new(name, self, self.class.attachment_definitions[name])
174
+ end
175
+
176
+ def each_attachment
177
+ self.class.attachment_definitions.each do |name, definition|
178
+ yield(name, attachment_for(name))
179
+ end
180
+ end
181
+
182
+ def save_attached_files
183
+ each_attachment do |name, attachment|
184
+ attachment.send(:flush_writes)
185
+ attachment.send(:flush_deletes)
186
+ end
187
+ end
188
+
189
+ def destroy_attached_files
190
+ each_attachment do |name, attachment|
191
+ attachment.send(:queue_existing_for_delete)
192
+ attachment.send(:flush_deletes)
193
+ end
194
+ end
195
+ end
196
+
197
+ end
@@ -0,0 +1,230 @@
1
+ module Paperclip
2
+ # The Attachment class manages the files for a given attachment. It saves when the model saves,
3
+ # deletes when the model is destroyed, and processes the file upon assignment.
4
+ class Attachment
5
+
6
+ attr_reader :name, :instance, :file, :styles, :default_style
7
+
8
+ # Creates an Attachment object. +name+ is the name of the attachment, +instance+ is the
9
+ # ActiveRecord object instance it's attached to, and +options+ is the same as the hash
10
+ # passed to +has_attached_file+.
11
+ def initialize name, instance, options
12
+ @name = name
13
+ @instance = instance
14
+ @url = options[:url] ||
15
+ "/:attachment/:id/:style/:basename.:extension"
16
+ @path = options[:path] ||
17
+ ":rails_root/public/:attachment/:id/:style/:basename.:extension"
18
+ @styles = options[:styles] || {}
19
+ @default_url = options[:default_url] || "/:attachment/:style/missing.png"
20
+ @validations = options[:validations] || []
21
+ @default_style = options[:default_style] || :original
22
+ @queued_for_delete = []
23
+ @processed_files = {}
24
+ @errors = []
25
+ @validation_errors = nil
26
+ @dirty = false
27
+
28
+ normalize_style_definition
29
+
30
+ @file = File.new(path) if original_filename && File.exists?(path)
31
+ end
32
+
33
+ # What gets called when you call instance.attachment = File. It clears errors,
34
+ # assigns attributes, processes the file, and runs validations. It also queues up
35
+ # the previous file for deletion, to be flushed away on #save of its host.
36
+ def assign uploaded_file
37
+ queue_existing_for_delete
38
+ @errors = []
39
+ @validation_errors = nil
40
+
41
+ return nil unless valid_file?(uploaded_file)
42
+
43
+ @file = uploaded_file.to_tempfile
44
+ @instance[:"#{@name}_file_name"] = uploaded_file.original_filename
45
+ @instance[:"#{@name}_content_type"] = uploaded_file.content_type
46
+ @instance[:"#{@name}_file_size"] = uploaded_file.size
47
+
48
+ @dirty = true
49
+
50
+ post_process
51
+ ensure
52
+ validate
53
+ end
54
+
55
+ # Returns the public URL of the attachment, with a given style. Note that this
56
+ # does not necessarily need to point to a file that your web server can access
57
+ # and can point to an action in your app, if you need fine grained security.
58
+ # This is not recommended if you don't need the security, however, for
59
+ # performance reasons.
60
+ def url style = nil
61
+ @file ? interpolate(@url, style) : interpolate(@default_url, style)
62
+ end
63
+
64
+ # Alias to +url+
65
+ def to_s style = nil
66
+ url(style)
67
+ end
68
+
69
+ # Returns true if there are any errors on this attachment.
70
+ def valid?
71
+ errors.length == 0
72
+ end
73
+
74
+ # Returns an array containing the errors on this attachment.
75
+ def errors
76
+ @errors.compact.uniq
77
+ end
78
+
79
+ # Returns true if there are changes that need to be saved.
80
+ def dirty?
81
+ @dirty
82
+ end
83
+
84
+ # Saves the file, if there are no errors. If there are, it flushes them to
85
+ # the instance's errors and returns false, cancelling the save.
86
+ def save
87
+ if valid?
88
+ flush_deletes
89
+ flush_writes
90
+ true
91
+ else
92
+ flush_errors
93
+ false
94
+ end
95
+ end
96
+
97
+ # Returns an +IO+ representing the data of the file assigned to the given
98
+ # style. Useful for streaming with +send_file+.
99
+ def to_io style = nil
100
+ begin
101
+ style ||= @default_style
102
+ @processed_files[style] || File.new(path(style))
103
+ rescue Errno::ENOENT
104
+ nil
105
+ end
106
+ end
107
+
108
+ # Returns the name of the file as originally assigned, and as lives in the
109
+ # <attachment>_file_name attribute of the model.
110
+ def original_filename
111
+ instance[:"#{name}_file_name"]
112
+ end
113
+
114
+ # A hash of procs that are run during the interpolation of a path or url.
115
+ # A variable of the format :name will be replaced with the return value of
116
+ # the proc named ":name". Each lambda takes the attachment and the current
117
+ # style as arguments. This hash can be added to with your own proc if
118
+ # necessary.
119
+ def self.interpolations
120
+ @interpolations ||= {
121
+ :rails_root => lambda{|attachment,style| RAILS_ROOT },
122
+ :class => lambda{|attachment,style| attachment.instance.class.to_s.downcase.pluralize },
123
+ :basename => lambda do |attachment,style|
124
+ attachment.original_filename.gsub(/\.(.*?)$/, "")
125
+ end,
126
+ :extension => lambda do |attachment,style|
127
+ ((style = attachment.styles[style]) && style.last) ||
128
+ File.extname(attachment.original_filename).gsub(/^\.+/, "")
129
+ end,
130
+ :id => lambda{|attachment,style| attachment.instance.id },
131
+ :partition_id => lambda do |attachment, style|
132
+ ("%09d" % attachment.instance.id).scan(/\d{3}/).join("/")
133
+ end,
134
+ :attachment => lambda{|attachment,style| attachment.name.to_s.downcase.pluralize },
135
+ :style => lambda{|attachment,style| style || attachment.default_style },
136
+ }
137
+ end
138
+
139
+ private
140
+
141
+ def valid_file? file #:nodoc:
142
+ file.respond_to?(:original_filename) && file.respond_to?(:content_type)
143
+ end
144
+
145
+ def validate #:nodoc:
146
+ unless @validation_errors
147
+ @validation_errors = @validations.collect do |v|
148
+ v.call(self, instance)
149
+ end.flatten.compact.uniq
150
+ @errors += @validation_errors
151
+ end
152
+ end
153
+
154
+ def normalize_style_definition
155
+ @styles.each do |name, args|
156
+ dimensions, format = [args, nil].flatten[0..1]
157
+ format = nil if format == ""
158
+ @styles[name] = [dimensions, format]
159
+ end
160
+ end
161
+
162
+ def post_process #:nodoc:
163
+ return nil if @file.nil?
164
+ @styles.each do |name, args|
165
+ begin
166
+ dimensions, format = args
167
+ @processed_files[name] = Thumbnail.make(self.file,
168
+ dimensions,
169
+ format,
170
+ @whiny_thumbnails)
171
+ rescue Errno::ENOENT => e
172
+ @errors << "could not be processed because the file does not exist."
173
+ rescue PaperclipError => e
174
+ @errors << e.message
175
+ end
176
+ end
177
+ @processed_files[:original] = @file
178
+ end
179
+
180
+ def interpolate pattern, style = nil #:nodoc:
181
+ style ||= @default_style
182
+ pattern = pattern.dup
183
+ self.class.interpolations.each do |tag, l|
184
+ pattern.gsub!(/:#{tag}/) do |match|
185
+ l.call( self, style )
186
+ end
187
+ end
188
+ pattern.gsub(%r{/+}, "/")
189
+ end
190
+
191
+ def path style = nil #:nodoc:
192
+ interpolate(@path, style)
193
+ end
194
+
195
+ def queue_existing_for_delete #:nodoc:
196
+ @queued_for_delete += @processed_files.values
197
+ @file = nil
198
+ @processed_files = {}
199
+ @instance[:"#{@name}_file_name"] = nil
200
+ @instance[:"#{@name}_content_type"] = nil
201
+ @instance[:"#{@name}_file_size"] = nil
202
+ end
203
+
204
+ def flush_errors #:nodoc:
205
+ @errors.each do |error|
206
+ instance.errors.add(name, error)
207
+ end
208
+ end
209
+
210
+ def flush_writes #:nodoc:
211
+ @processed_files.each do |style, file|
212
+ FileUtils.mkdir_p( File.dirname(path(style)) )
213
+ @processed_files[style] = file.stream_to(path(style)) unless file.path == path(style)
214
+ end
215
+ @file = @processed_files[@default_style]
216
+ end
217
+
218
+ def flush_deletes #:nodoc:
219
+ @queued_for_delete.compact.each do |file|
220
+ begin
221
+ FileUtils.rm(file.path)
222
+ rescue Errno::ENOENT => e
223
+ # ignore them
224
+ end
225
+ end
226
+ @queued_for_delete = []
227
+ end
228
+ end
229
+ end
230
+
@@ -0,0 +1,109 @@
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 = nil if height == ""
10
+ width = nil if width == ""
11
+ @height = (height || width).to_f
12
+ @width = (width || height).to_f
13
+ @modifier = modifier
14
+ end
15
+
16
+ # Uses ImageMagick to determing the dimensions of a file, passed in as either a
17
+ # File or path.
18
+ def self.from_file file
19
+ file = file.path if file.respond_to? "path"
20
+ parse(`#{Paperclip.path_for_command('identify')} "#{file}"`) ||
21
+ raise(Errno::ENOENT, file)
22
+ end
23
+
24
+ # Parses a "WxH" formatted string, where W is the width and H is the height.
25
+ def self.parse string
26
+ if match = (string && string.match(/\b(\d*)x(\d*)\b([\>\<\#\@\%^!])?/))
27
+ Geometry.new(*match[1,3])
28
+ end
29
+ end
30
+
31
+ # True if the dimensions represent a square
32
+ def square?
33
+ height == width
34
+ end
35
+
36
+ # True if the dimensions represent a horizontal rectangle
37
+ def horizontal?
38
+ height < width
39
+ end
40
+
41
+ # True if the dimensions represent a vertical rectangle
42
+ def vertical?
43
+ height > width
44
+ end
45
+
46
+ # The aspect ratio of the dimensions.
47
+ def aspect
48
+ width / height
49
+ end
50
+
51
+ # Returns the larger of the two dimensions
52
+ def larger
53
+ [height, width].max
54
+ end
55
+
56
+ # Returns the smaller of the two dimensions
57
+ def smaller
58
+ [height, width].min
59
+ end
60
+
61
+ # Returns the width and height in a format suitable to be passed to Geometry.parse
62
+ def to_s
63
+ "%dx%d%s" % [width, height, modifier]
64
+ end
65
+
66
+ # Same as to_s
67
+ def inspect
68
+ to_s
69
+ end
70
+
71
+ # Returns the scaling and cropping geometries (in string-based ImageMagick format)
72
+ # neccessary to transform this Geometry into the Geometry given. If crop is true,
73
+ # then it is assumed the destination Geometry will be the exact final resolution.
74
+ # In this case, the source Geometry is scaled so that an image containing the
75
+ # destination Geometry would be completely filled by the source image, and any
76
+ # overhanging image would be cropped. Useful for square thumbnail images. The cropping
77
+ # is weighted at the center of the Geometry.
78
+ def transformation_to dst, crop = false
79
+ ratio = Geometry.new( dst.width / self.width, dst.height / self.height )
80
+
81
+ if crop
82
+ scale_geometry, scale = scaling(dst, ratio)
83
+ crop_geometry = cropping(dst, ratio, scale)
84
+ else
85
+ scale_geometry = dst.to_s
86
+ end
87
+
88
+ [ scale_geometry, crop_geometry ]
89
+ end
90
+
91
+ private
92
+
93
+ def scaling dst, ratio
94
+ if ratio.horizontal? || ratio.square?
95
+ [ "%dx" % dst.width, ratio.width ]
96
+ else
97
+ [ "x%d" % dst.height, ratio.height ]
98
+ end
99
+ end
100
+
101
+ def cropping dst, ratio, scale
102
+ if ratio.horizontal? || ratio.square?
103
+ "%dx%d+%d+%d" % [ dst.width, dst.height, 0, (self.height * scale - dst.height) / 2 ]
104
+ else
105
+ "%dx%d+%d+%d" % [ dst.width, dst.height, (self.width * scale - dst.width) / 2, 0 ]
106
+ end
107
+ end
108
+ end
109
+ end