attachment-san 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,44 @@
1
+ Copyright (c) 2008 Manfred Stienstra, Fingertips <manfred@fngtps.com>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ this software and associated documentation files (the "Software"), to deal in
5
+ the Software without restriction, including without limitation the rights to
6
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7
+ the Software, and to permit persons to whom the Software is furnished to do so,
8
+ subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19
+
20
+ --------------------------------------------------------------------------------
21
+
22
+ Some parts of attachment-san were originally included in ‘attachment_fu’ and
23
+ ‘acts_as_attachment’ under the following license:
24
+
25
+ Copyright (c) 2005 Rick Olson
26
+
27
+ Unless noted specifically, all plugins in this repository are MIT licensed:
28
+
29
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
30
+ this software and associated documentation files (the "Software"), to deal in
31
+ the Software without restriction, including without limitation the rights to
32
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
33
+ the Software, and to permit persons to whom the Software is furnished to do so,
34
+ subject to the following conditions:
35
+
36
+ The above copyright notice and this permission notice shall be included in all
37
+ copies or substantial portions of the Software.
38
+
39
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
40
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
41
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
42
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
43
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
44
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,10 @@
1
+ Attachment-San
2
+
3
+ Plugin for easy and rich attachment manipulation.
4
+
5
+ Although the ideas behind Attachment-San are different, it borrows heavily on both attachment_fu and acts_as_attachment by Rick Olson.
6
+
7
+ Dependencies
8
+
9
+ General: A recent Rails (at least 2.0, though it's most often tested on Edge)
10
+ Development: test/spec and SQLite3
data/Rakefile ADDED
@@ -0,0 +1,31 @@
1
+ require 'rake/rdoctask'
2
+ require 'rake/testtask'
3
+
4
+ desc "Run all tests by default"
5
+ task :default => :test
6
+
7
+ Rake::TestTask.new do |t|
8
+ t.test_files = FileList['test/**/*_test.rb']
9
+ t.verbose = true
10
+ end
11
+
12
+ begin
13
+ require 'jeweler'
14
+ Jeweler::Tasks.new do |s|
15
+ s.name = "attachment-san"
16
+ s.homepage = "http://github.com/Fingertips/attachment-san"
17
+ s.email = ["eloy@fngtps.com", "manfred@fngtps.com"]
18
+ s.authors = ["Eloy Duran", "Manfred Stienstra"]
19
+ s.summary = s.description = "Rails plugin for easy and rich attachment manipulation."
20
+ s.files -= %w{ .gitignore .kick }
21
+ end
22
+ rescue LoadError
23
+ end
24
+
25
+ namespace :docs do
26
+ Rake::RDocTask.new(:generate) do |rd|
27
+ rd.main = "README"
28
+ rd.rdoc_files.include("README", "LICENSE", "lib/**/*.rb")
29
+ rd.options << "--all" << "--charset" << "utf-8"
30
+ end
31
+ end
data/TODO ADDED
@@ -0,0 +1,6 @@
1
+ * Make it actually work with more than one attachment class.
2
+ * has_attachment :watermark, :superclass => WatermarkSuperclass
3
+ * has_attachment :watermark, :class => WatermarkClass
4
+ * Nest created classes under the superclass.
5
+ * Do we need to sanitize the original filename, eg lowercase.
6
+ * Make :filename_scheme => :record_identifier also use nesting under parent resources. Eg /resources/1/images/2/filename
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
@@ -0,0 +1,73 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{attachment-san}
8
+ s.version = "0.0.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Eloy Duran", "Manfred Stienstra"]
12
+ s.date = %q{2009-11-19}
13
+ s.description = %q{Rails plugin for easy and rich attachment manipulation.}
14
+ s.email = ["eloy@fngtps.com", "manfred@fngtps.com"]
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README"
18
+ ]
19
+ s.files = [
20
+ "LICENSE",
21
+ "README",
22
+ "Rakefile",
23
+ "TODO",
24
+ "VERSION",
25
+ "examples/photo.rb",
26
+ "examples/photos_controller.rb",
27
+ "examples/post.rb",
28
+ "lib/attachment_san.rb",
29
+ "lib/attachment_san/has.rb",
30
+ "lib/attachment_san/test/helper.rb",
31
+ "lib/attachment_san/test/rails.png",
32
+ "lib/attachment_san/variant.rb",
33
+ "napkin.rb",
34
+ "rails/init.rb",
35
+ "test/attachment_san_test.rb",
36
+ "test/fixtures/models/attachment.rb",
37
+ "test/fixtures/models/document.rb",
38
+ "test/fixtures/models/options_stub.rb",
39
+ "test/has_test.rb",
40
+ "test/lib/schema.rb",
41
+ "test/test_helper.rb",
42
+ "test/variant_test.rb"
43
+ ]
44
+ s.homepage = %q{http://github.com/Fingertips/attachment-san}
45
+ s.rdoc_options = ["--charset=UTF-8"]
46
+ s.require_paths = ["lib"]
47
+ s.rubygems_version = %q{1.3.5}
48
+ s.summary = %q{Rails plugin for easy and rich attachment manipulation.}
49
+ s.test_files = [
50
+ "test/attachment_san_test.rb",
51
+ "test/fixtures/models/attachment.rb",
52
+ "test/fixtures/models/document.rb",
53
+ "test/fixtures/models/options_stub.rb",
54
+ "test/has_test.rb",
55
+ "test/lib/schema.rb",
56
+ "test/test_helper.rb",
57
+ "test/variant_test.rb",
58
+ "examples/photo.rb",
59
+ "examples/photos_controller.rb",
60
+ "examples/post.rb"
61
+ ]
62
+
63
+ if s.respond_to? :specification_version then
64
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
65
+ s.specification_version = 3
66
+
67
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
68
+ else
69
+ end
70
+ else
71
+ end
72
+ end
73
+
data/examples/photo.rb ADDED
@@ -0,0 +1,41 @@
1
+ class Photo < ActiveRecord::Base
2
+ attachment_san
3
+ after_upload :make_black
4
+
5
+ has_one :thumbnail, :class => 'Photo'
6
+ belongs_to :large_photo, :class => 'Photo'
7
+ before_create :do_create_thumbnail
8
+
9
+ def do_create_thumbnail
10
+ unless large_photo
11
+ create_thumbnail :uploaded_data = uploaded_data, :size => "<300x400"
12
+ end
13
+ end
14
+ end
15
+
16
+ # ---
17
+
18
+ class Photo < ActiveRecord::Base
19
+ has_one :thumbnail
20
+ has_one :full_size
21
+
22
+ def uploaded_data=(data)
23
+ build_thumbnail :uploaded_data => data
24
+ build_full_size :uploaded_data => data
25
+ end
26
+ end
27
+
28
+ class Attachment < ActiveRecord::Base
29
+ end
30
+
31
+ class Thumbnail < Attachment
32
+ attachment_san
33
+
34
+ def resize(image)
35
+ attachment.resize(image)
36
+ end
37
+ end
38
+
39
+ class FullSize < Attachment
40
+ attachment_san :keep_filenames => true
41
+ end
@@ -0,0 +1,22 @@
1
+ class PhotosController < ActionController::Base
2
+ before_filter :find_post
3
+
4
+ def new
5
+ end
6
+
7
+ def create
8
+ @photo = @post.photos.build params[:photo]
9
+ if @photo.save
10
+ flash[:hilight] = dom_id(@photo)
11
+ redirect_to @post
12
+ else
13
+ render :new
14
+ end
15
+ end
16
+
17
+ protected
18
+
19
+ def find_post
20
+ @post = Post.find params[:post_id]
21
+ end
22
+ end
data/examples/post.rb ADDED
@@ -0,0 +1,3 @@
1
+ class Post < ActiveRecord::Base
2
+ has_many :photos, :dependent => :destroy
3
+ end
@@ -0,0 +1,49 @@
1
+ module AttachmentSan
2
+ module Has
3
+ VALID_OPTIONS = [:name, :process, :class, :variants, :filename_scheme]
4
+
5
+ def has_attachment(name, options = {})
6
+ define_attachment_association :has_one, name, options
7
+ end
8
+
9
+ def has_attachments(name, options = {})
10
+ define_attachment_association :has_many, name, options
11
+ end
12
+
13
+ private
14
+
15
+ def define_attachment_association(macro, name, options)
16
+ define_variants(name, extract_variant_options!(options))
17
+ send(macro, name, options) unless reflect_on_association(name)
18
+ end
19
+
20
+ def extract_variant_options!(options)
21
+ options.symbolize_keys!
22
+ variant_options = options.slice(*VALID_OPTIONS)
23
+ options.except!(*VALID_OPTIONS)
24
+ variant_options
25
+ end
26
+
27
+ def define_variants(name, options)
28
+ model = create_model(name)
29
+
30
+ original = { :class => Variant::Original }
31
+ original.merge!(options) unless options.has_key?(:variants)
32
+ model.send(:define_variant, :original, original)
33
+
34
+ if variants = options[:variants]
35
+ variants.each do |name, options|
36
+ model.send(:define_variant, name, options)
37
+ end
38
+ end
39
+ end
40
+
41
+ # TODO: Currently creates these classes in the top level namespace
42
+ def create_model(name)
43
+ name = name.to_s.classify
44
+ ::Object.const_get(name)
45
+ rescue NameError
46
+ ::Object.const_set name, Class.new(AttachmentSan.attachment_class)
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,49 @@
1
+ require 'test/unit'
2
+ require 'tempfile'
3
+ require 'fileutils'
4
+
5
+ module AttachmentSan
6
+ module UploadHelpers
7
+ # Generates a Tempfile object similar to the object you'd get from the standard library CGI
8
+ # module in a multipart request.
9
+ #
10
+ # Borrowed from action_controller/test_process.
11
+ class FakeUploadFile
12
+ # The filename, *not* including the path, of the "uploaded" file
13
+ attr_reader :original_filename
14
+
15
+ # The content type of the "uploaded" file
16
+ attr_reader :content_type
17
+
18
+ def initialize(path, content_type = Mime::TEXT, binary = false)
19
+ path = path.to_s
20
+ raise "#{path} file does not exist" unless File.exist?(path)
21
+ @content_type = content_type
22
+ @original_filename = path.sub(/^.*#{File::SEPARATOR}([^#{File::SEPARATOR}]+)$/) { $1 }
23
+ @tempfile = Tempfile.new(@original_filename)
24
+ @tempfile.binmode if binary
25
+ FileUtils.copy_file(path, @tempfile.path)
26
+ end
27
+
28
+ def path #:nodoc:
29
+ @tempfile.path
30
+ end
31
+
32
+ alias local_path path
33
+
34
+ def method_missing(method_name, *args, &block) #:nodoc:
35
+ @tempfile.send(method_name, *args, &block)
36
+ end
37
+ end
38
+
39
+ def uploaded_file(filename, content_type)
40
+ FakeUploadFile.new(filename, content_type)
41
+ end
42
+
43
+ def rails_icon
44
+ uploaded_file(File.expand_path('../rails.png', __FILE__), 'image/png')
45
+ end
46
+ end
47
+ end
48
+
49
+ Test::Unit::TestCase.send(:include, AttachmentSan::UploadHelpers)
Binary file
@@ -0,0 +1,153 @@
1
+ require "digest"
2
+ require "fileutils"
3
+
4
+ module AttachmentSan
5
+ class Variant
6
+ module ClassMethods
7
+ def self.extended(model)
8
+ model.class_inheritable_accessor :variant_reflections
9
+ model.variant_reflections = []
10
+ end
11
+
12
+ def reflect_on_variant(name)
13
+ variant_reflections.find { |r| r[:name] == name.to_sym }
14
+ end
15
+
16
+ private
17
+
18
+ def define_variant(name, options_or_class_or_proc)
19
+ return if reflect_on_variant(name)
20
+
21
+ reflection =
22
+ case x = options_or_class_or_proc
23
+ when Hash
24
+ { :class => Variant }.merge(x.symbolize_keys)
25
+ when Class
26
+ { :class => x }
27
+ when Proc
28
+ { :class => Variant, :process => x }
29
+ when nil
30
+ { :class => const_get(name.to_s.camelize) }
31
+ else
32
+ raise TypeError, "Please specify a options hash, variant class, or process proc. Can't use `#{x.inspect}'."
33
+ end
34
+
35
+ reflection[:name] = name = name.to_sym
36
+ variant_reflections << reflection
37
+
38
+ # def original
39
+ # @original ||= begin
40
+ # reflection = self.class.reflect_on_variant(:original)
41
+ # reflection.klass.new(self, reflection)
42
+ # end
43
+ # end
44
+ class_eval <<-DEF, __FILE__, __LINE__ + 1
45
+ def #{name}
46
+ @#{name} ||= begin
47
+ reflection = self.class.reflect_on_variant(:#{name})
48
+ reflection[:class].new(self, reflection)
49
+ end
50
+ end
51
+ DEF
52
+ end
53
+ end
54
+ end
55
+
56
+ class Variant
57
+ attr_reader :record, :reflection
58
+
59
+ def initialize(record, reflection)
60
+ @record, @reflection = record, reflection
61
+ end
62
+
63
+ def base_options
64
+ @record.class.attachment_san_options
65
+ end
66
+
67
+ def base_path
68
+ base_options[:base_path]
69
+ end
70
+
71
+ def public_base_path
72
+ base_options[:public_base_path]
73
+ end
74
+
75
+ def filename_scheme
76
+ @reflection[:filename_scheme] || base_options[:filename_scheme]
77
+ end
78
+
79
+ def original
80
+ @record.original
81
+ end
82
+
83
+ def name
84
+ @reflection[:name]
85
+ end
86
+
87
+ def extension
88
+ (ext = base_options[:extension]) == :keep_original ? @record.extension : ext
89
+ end
90
+
91
+ def token
92
+ @record.token.scan(/.{2}/)
93
+ end
94
+
95
+ def filename
96
+ unless @filename
97
+ @filename =
98
+ case filename_scheme
99
+ when :variant_name
100
+ name.to_s
101
+ when :keep_original
102
+ @record.filename
103
+ when :record_identifier
104
+ @record_class_name ||= @record.class.name.underscore.pluralize
105
+ "/#{@record_class_name}/#{@record.to_param}/#{name}"
106
+ when :token
107
+ File.join(token, "#{@record.filename_without_extension}.#{name}")
108
+ else
109
+ raise ArgumentError, "The :filename_scheme option should be one of `:token', `:filename_scheme', `:record_identifier', or `:variant_name', it currently is `#{filename_scheme.inspect}'."
110
+ end
111
+ @filename << ".#{extension}" unless extension.blank?
112
+ end
113
+ @filename
114
+ end
115
+
116
+ def file_path
117
+ File.join(base_path, filename)
118
+ end
119
+
120
+ def public_path
121
+ File.join(public_base_path, filename)
122
+ end
123
+
124
+ def dir_path
125
+ File.dirname(file_path)
126
+ end
127
+
128
+ def mkdir!
129
+ FileUtils.mkdir_p(dir_path)
130
+ end
131
+
132
+ def process!
133
+ mkdir!
134
+ @reflection[:process].call(self)
135
+ end
136
+
137
+ class Original < Variant
138
+ def original
139
+ self
140
+ end
141
+
142
+ def filename
143
+ @filename ||= (filename_scheme == :token ? File.join(token, @record.filename) : super)
144
+ end
145
+
146
+ def process!
147
+ return super if @reflection[:process]
148
+ mkdir!
149
+ File.open(file_path, 'w') { |f| f.write @record.uploaded_file.read }
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,58 @@
1
+ require "attachment_san/has"
2
+ require "attachment_san/variant"
3
+
4
+ module AttachmentSan
5
+ module Initializer
6
+ def attachment_san(options = {})
7
+ include AttachmentSan
8
+
9
+ opt = self.attachment_san_options = {
10
+ :public_base_path => '',
11
+ :extension => :keep_original,
12
+ :filename_scheme => :variant_name
13
+ }.merge(options)
14
+ # Defaults :base_path to expanded :public_base_path.
15
+ opt[:base_path] ||= Rails.root + File.join('public', opt[:public_base_path])
16
+ opt
17
+ end
18
+ end
19
+
20
+ mattr_accessor :attachment_class
21
+
22
+ def self.included(model)
23
+ self.attachment_class = model
24
+ model.extend Variant::ClassMethods
25
+
26
+ model.class_inheritable_accessor :attachment_san_options
27
+ model.define_callbacks :before_upload, :after_upload
28
+ model.after_create :process_variants!
29
+ end
30
+
31
+ attr_reader :uploaded_file
32
+
33
+ def uploaded_file=(uploaded_file)
34
+ callback :before_upload
35
+
36
+ @uploaded_file = uploaded_file
37
+ self.filename = uploaded_file.original_filename
38
+ self.content_type = uploaded_file.content_type
39
+
40
+ callback :after_upload
41
+ end
42
+
43
+ def extension
44
+ filename.split('.').last if filename.include?('.')
45
+ end
46
+
47
+ def filename_without_extension
48
+ filename.include?('.') ? filename.split('.')[0..-2].join('.') : filename
49
+ end
50
+
51
+ def variants
52
+ self.class.variant_reflections.map { |reflection| send(reflection[:name]) }
53
+ end
54
+
55
+ def process_variants!
56
+ variants.each(&:process!)
57
+ end
58
+ end
data/napkin.rb ADDED
@@ -0,0 +1,41 @@
1
+ # Variant:
2
+ # * Returns path information
3
+ # * /a2/e0/dd/fits_within_200x200.jpg - "many many files"
4
+ # * /organizations/12/logos/main.jpg - "one or two files"
5
+ # * /cant/guess/me/82734mhsdf923184jnshdf91238ukjhsdf.jpg - "secret location"
6
+
7
+
8
+ member.avatar.medium # => member.avatar.image('fits_within_8x8')
9
+
10
+ company.logo.original # =>
11
+
12
+ { :company => { :logo => { :data => IOWEETIKVEEL } } }
13
+
14
+ # abstract model for data about a file, and has variants, defined by the model that has attachments
15
+ class Attachment
16
+ belongs_to :attachable
17
+
18
+ include Att # makes this an attachment model
19
+ end
20
+
21
+ class Member
22
+ extend Att::Has # adds declerative class methods
23
+ include Att::Variant # defines filters to handle variants
24
+
25
+ attaches_one :avatar
26
+ attaches_many
27
+
28
+ has_attachment :avatar, :variants => {
29
+ 'medium' => { ... }, # => Class.new(Att:Variant)
30
+ 'fits_within_80x80' => { :fits_within => [80, 80] }
31
+ }
32
+ end
33
+
34
+ # TODO
35
+ # * filters on a variant basis?
36
+ # * actual sizes
37
+ # * module Variants
38
+ # class Medium
39
+ # undef :foo
40
+ # end
41
+ # end
data/rails/init.rb ADDED
@@ -0,0 +1,4 @@
1
+ require 'attachment_san'
2
+ ActiveRecord::Base.extend AttachmentSan::Initializer
3
+
4
+ require 'attachment_san/test/helper' if Rails.env == 'test'
@@ -0,0 +1,148 @@
1
+ require File.expand_path('../test_helper', __FILE__)
2
+
3
+ describe "AttachmentSan, concerning base options" do
4
+ default = OptionsStub.new_subclass do
5
+ attachment_san
6
+ end
7
+
8
+ default_with_public_path = OptionsStub.new_subclass do
9
+ attachment_san :public_base_path => '/files/assets'
10
+ end
11
+
12
+ specified = OptionsStub.new_subclass do
13
+ attachment_san :base_path => '/some/base/path', :public_base_path => '/files/assets', :extension => :png, :filename_scheme => :token
14
+ end
15
+
16
+ it "should default the base_path to `public'" do
17
+ default.attachment_san_options[:base_path].should == Rails.root + 'public/'
18
+ end
19
+
20
+ it "should use the specified base_path" do
21
+ specified.attachment_san_options[:base_path].should == '/some/base/path'
22
+ end
23
+
24
+ it "should default the public_base_path to an empty string" do
25
+ default.attachment_san_options[:public_base_path].should == ''
26
+ end
27
+
28
+ it "should use the specified public_base_path" do
29
+ specified.attachment_san_options[:public_base_path].should == '/files/assets'
30
+ end
31
+
32
+ it "should default the base_path to `public' + public_base_path" do
33
+ default_with_public_path.attachment_san_options[:base_path].should == Rails.root + 'public/files/assets'
34
+ end
35
+
36
+ it "should default to use the variant's name as the filename for variants" do
37
+ default.attachment_san_options[:filename_scheme].should == :variant_name
38
+ end
39
+
40
+ it "should use a token version for variant filenames" do
41
+ specified.attachment_san_options[:filename_scheme].should == :token
42
+ end
43
+
44
+ it "should use the unique records identifier for variant filenames" do
45
+ OptionsStub.new_subclass do
46
+ attachment_san :filename_scheme => :record_identifier
47
+ end.attachment_san_options[:filename_scheme].should == :record_identifier
48
+ end
49
+
50
+ it "should default to use the original file's extension for variants" do
51
+ default.attachment_san_options[:extension].should == :keep_original end
52
+
53
+ it "should use the specified file extension for variants" do
54
+ specified.attachment_san_options[:extension].should == :png
55
+ end
56
+ end
57
+
58
+ describe "AttachmentSan, class methods" do
59
+ before do
60
+ @upload = rails_icon
61
+ @attachment = Attachment.new(:uploaded_file => @upload)
62
+ end
63
+
64
+ it "should assign the base attachment class when attachment_san is called" do
65
+ AttachmentSan.attachment_class.should.be Attachment
66
+ end
67
+
68
+ it "should call a before_upload filter chain before actually assigning the new uploaded file" do
69
+ new_upload = rails_icon
70
+ @attachment.uploaded_file = new_upload
71
+ @attachment.file_before_upload.should.be @upload
72
+ end
73
+
74
+ it "should call an after_upload filter chain after assigning the new uploaded file" do
75
+ @attachment.file_after_upload.should.be @upload
76
+ end
77
+
78
+ it "should define a variant with options" do
79
+ Logo.variant_reflections.map { |r| r[:name] }.should == [:original, :header]
80
+ Logo.new.header.should.be.instance_of MyVariant
81
+ end
82
+ end
83
+
84
+ describe "AttachmentSan, instance methods" do
85
+ before do
86
+ @upload = rails_icon
87
+ @attachment = Attachment.new(:uploaded_file => @upload)
88
+ end
89
+
90
+ it "should make a model accept a file upload" do
91
+ @attachment.uploaded_file.should.be @upload
92
+ end
93
+
94
+ it "should assign the original filename to the model" do
95
+ @attachment.filename.should == @upload.original_filename
96
+ end
97
+
98
+ it "should assign the content type to the model" do
99
+ @attachment.content_type.should == @upload.content_type
100
+ end
101
+
102
+ it "should return the original file's extension" do
103
+ @attachment.extension.should == 'png'
104
+
105
+ @attachment.stubs(:filename).returns('Rakefile')
106
+ @attachment.extension.should.be nil
107
+ end
108
+
109
+ it "should return the original filename without extension" do
110
+ @attachment.filename_without_extension.should == 'rails'
111
+
112
+ @attachment.stubs(:filename).returns('Rakefile')
113
+ @attachment.filename_without_extension.should == 'Rakefile'
114
+
115
+ @attachment.stubs(:filename).returns('some.dotted.spaced.file.txt')
116
+ @attachment.filename_without_extension.should == 'some.dotted.spaced.file'
117
+ end
118
+ end
119
+
120
+ describe "An AttachmentSan instance, concerning variants" do
121
+ before do
122
+ @upload = rails_icon
123
+ @document = Document.new
124
+ @document.build_logo :uploaded_file => @upload
125
+ @document.build_watermark :uploaded_file => @upload
126
+ end
127
+
128
+ it "should call process on each of it's variants after create" do
129
+ @document.watermark.original.expects(:process!)
130
+ @document.logo.original.expects(:process!)
131
+ @document.logo.header.expects(:process!)
132
+ @document.save!
133
+ end
134
+
135
+ it "should not call process when the model already exists" do
136
+ @document.save!
137
+
138
+ @document.watermark.original.expects(:process!).never
139
+ @document.logo.original.expects(:process!).never
140
+ @document.logo.header.expects(:process!).never
141
+ @document.save!
142
+ end
143
+
144
+ it "should return it's variants" do
145
+ @document.watermark.variants.map(&:name).should == [:original]
146
+ @document.logo.variants.map(&:name).should == [:original, :header]
147
+ end
148
+ end
@@ -0,0 +1,20 @@
1
+ class Attachment < ActiveRecord::Base
2
+ attachment_san :base_path => TMP_DIR, :public_base_path => '/files/assets'
3
+
4
+ # Test code
5
+
6
+ def self.reset!
7
+ FileUtils.rm_rf TMP_DIR
8
+ FileUtils.mkdir_p TMP_DIR
9
+ end
10
+
11
+ # User code
12
+
13
+ belongs_to :attachable, :polymorphic => true
14
+
15
+ attr_accessor :file_before_upload
16
+ before_upload { |record| record.file_before_upload = record.uploaded_file }
17
+
18
+ attr_accessor :file_after_upload
19
+ after_upload { |record| record.file_after_upload = record.uploaded_file }
20
+ end
@@ -0,0 +1,39 @@
1
+ class MyVariant < AttachmentSan::Variant
2
+ def process!
3
+ end
4
+ end
5
+
6
+ class MyProcessor
7
+ def initialize(variant)
8
+ end
9
+ end
10
+
11
+ class AddressCard < Attachment
12
+ class SmallCard < AttachmentSan::Variant
13
+ def process!
14
+ puts 'SMALL!'
15
+ end
16
+ end
17
+
18
+ class BigCard < AttachmentSan::Variant
19
+ def process!
20
+ puts 'BIG!'
21
+ end
22
+ end
23
+ end
24
+
25
+ class Document < ActiveRecord::Base
26
+ extend AttachmentSan::Has
27
+
28
+ has_attachment :watermark
29
+ has_attachment :logo, :variants => { :header => MyVariant }
30
+
31
+ has_attachments :misc_files, :filename_scheme => :keep_original, :process => proc { :from_process_proc }
32
+ has_attachments :images, :variants => {
33
+ :thumbnail => proc { |v| MyProcessor.new(v) },
34
+ :medium_sized => proc {},
35
+ :download => proc {}
36
+ }
37
+
38
+ has_attachments :address_cards, :variants => [:small_card, :big_card]
39
+ end
@@ -0,0 +1,20 @@
1
+ class OptionsStub
2
+ def self.new_subclass(&block)
3
+ before = AttachmentSan.attachment_class
4
+ model = Class.new(self)
5
+ model.instance_eval(&block)
6
+ AttachmentSan.attachment_class = before
7
+ model
8
+ end
9
+
10
+ def self.define_callbacks(*args)
11
+ # no we don't!
12
+ end
13
+
14
+ def self.after_create(m)
15
+ # no we don't!
16
+ end
17
+
18
+ class_inheritable_accessor :attachment_san_options
19
+ extend AttachmentSan::Initializer
20
+ end
data/test/has_test.rb ADDED
@@ -0,0 +1,116 @@
1
+ require File.expand_path('../test_helper', __FILE__)
2
+
3
+ describe "AttachmentSan::Has" do
4
+ it "should pass on any unknown options to the has_one macro" do
5
+ Document.expects(:has_one).with(:other_file, :as => :attachable, :order => :updated_at)
6
+ Document.expects(:define_variants).with(:other_file, :process => proc {}, :class => MyVariant, :filename_scheme => :token, :variants => [:hoge, :fuga])
7
+
8
+ Document.has_attachment :other_file, :as => :attachable, :order => :updated_at,
9
+ :process => proc {}, :class => MyVariant, :filename_scheme => :token, :variants => [:hoge, :fuga]
10
+ end
11
+
12
+ it "should pass on any unknown options to the has_many macro" do
13
+ Document.expects(:has_many).with(:other_files, :as => :attachable, :order => :updated_at)
14
+ Document.expects(:define_variants).with(:other_files, :process => proc {}, :class => MyVariant, :filename_scheme => :token, :variants => [:hoge, :fuga])
15
+
16
+ Document.has_attachments :other_files, :as => :attachable, :order => :updated_at,
17
+ :process => proc {}, :class => MyVariant, :filename_scheme => :token, :variants => [:hoge, :fuga]
18
+ end
19
+
20
+ it "should not define an association if an association for the given name exists" do
21
+ Document.expects(:has_one).never
22
+ Document.expects(:define_variants).with(:watermark, {})
23
+
24
+ Document.expects(:has_many).never
25
+ Document.expects(:define_variants).with(:images, {})
26
+
27
+ Document.has_attachment :watermark, :as => :attachable
28
+ Document.has_attachments :images, :as => :attachable
29
+ end
30
+ end
31
+
32
+ describe "AttachmentSan::Has, concerning a single associated attachment" do
33
+ before do
34
+ @document = Document.new
35
+ @document.build_logo :uploaded_file => rails_icon
36
+ @document.build_watermark :uploaded_file => rails_icon
37
+ end
38
+
39
+ it "should create a model class and a has_one association" do
40
+ reflection = Document.reflect_on_association(:logo)
41
+ reflection.macro.should == :has_one
42
+ reflection.klass.should == Logo
43
+ Logo.superclass.should.be AttachmentSan.attachment_class
44
+ end
45
+
46
+ it "should store the variant options on the new model class" do
47
+ Watermark.variant_reflections.length.should == 1
48
+ Watermark.variant_reflections.first[:name].should == :original
49
+ Watermark.variant_reflections.first[:class].should == AttachmentSan::Variant::Original
50
+
51
+ Logo.variant_reflections.length.should == 2
52
+ Logo.variant_reflections.last[:name].should == :header
53
+ Logo.variant_reflections.last[:class].should == MyVariant
54
+ end
55
+
56
+ it "should define only a default original variant if no other variants are given" do
57
+ variant = @document.watermark.original
58
+ variant.name.should == :original
59
+ variant.should.be.instance_of AttachmentSan::Variant::Original
60
+ end
61
+
62
+ it "should define a default original variant and the ones specified" do
63
+ %w{ original header }.each do |name|
64
+ variant = @document.logo.send(name)
65
+ variant.name.to_s.should == name
66
+ variant.should.be.instance_of Logo.reflect_on_variant(name)[:class]
67
+ end
68
+ end
69
+ end
70
+
71
+ describe "AttachmentSan::Has, concerning a collection of associated attachments" do
72
+ before do
73
+ @document = Document.new
74
+ 2.times { @document.images.build :uploaded_file => rails_icon }
75
+ 2.times { @document.misc_files.build :uploaded_file => rails_icon }
76
+ end
77
+
78
+ it "should create a model class and a has_many association" do
79
+ reflection = Document.reflect_on_association(:images)
80
+ reflection.macro.should == :has_many
81
+ reflection.klass.should == Image
82
+ end
83
+
84
+ it "should define only a default original variant if no others are given" do
85
+ variants = @document.misc_files.map(&:original)
86
+ variants.should.all { |v| v.name == :original }
87
+ variants.should.all { |v| v.instance_of? AttachmentSan::Variant }
88
+ end
89
+
90
+ it "should define a default original variant and the ones specified" do
91
+ %w{ original thumbnail medium_sized download }.each do |name|
92
+ variants = @document.images.map(&name.to_sym)
93
+ variants.should.all { |v| v.name == name }
94
+ variants.should.all { |v| v.instance_of? AttachmentSan::Variant }
95
+ end
96
+ end
97
+ end
98
+
99
+ describe "AttachmentSan::Has, concerning attachment definitions with only a default original variant" do
100
+ it "should pass the options hash on to the variant" do
101
+ MiscFile.variant_reflections.length.should == 1
102
+ MiscFile.variant_reflections.first[:name].should == :original
103
+ MiscFile.variant_reflections.first[:class].should == AttachmentSan::Variant::Original
104
+ MiscFile.variant_reflections.first[:filename_scheme].should == :keep_original
105
+ MiscFile.variant_reflections.first[:process].call.should == :from_process_proc
106
+ end
107
+ end
108
+
109
+ describe "AttachmentSan::Has, concerning attachment definitions with an array of variants" do
110
+ it "should simply assume the variant class exists" do
111
+ @document = Document.new
112
+ card = @document.address_cards.build(:uploaded_file => rails_icon)
113
+ card.small_card.should.be.instance_of AddressCard::SmallCard
114
+ card.big_card.should.be.instance_of AddressCard::BigCard
115
+ end
116
+ end
@@ -0,0 +1,14 @@
1
+ ActiveRecord::Migration.verbose = false
2
+ ActiveRecord::Schema.define(:version => 1) do
3
+
4
+ create_table :attachments, :force => true do |t|
5
+ t.string :filename
6
+ t.string :content_type
7
+ t.string :attachable_type
8
+ t.integer :attachable_id
9
+ end
10
+
11
+ create_table :documents, :force => true do |t|
12
+ end
13
+
14
+ end
@@ -0,0 +1,66 @@
1
+ TEST_ROOT_DIR = File.expand_path(File.dirname(__FILE__))
2
+
3
+ frameworks = %w(activesupport activerecord actionpack)
4
+
5
+ rails = [
6
+ File.expand_path('../../../rails', TEST_ROOT_DIR),
7
+ File.expand_path('../../rails', TEST_ROOT_DIR)
8
+ ].detect do |possible_rails|
9
+ begin
10
+ entries = Dir.entries(possible_rails)
11
+ frameworks.all? { |framework| entries.include?(framework) }
12
+ rescue Errno::ENOENT
13
+ false
14
+ end
15
+ end
16
+
17
+ raise "Couldn't find Rails directory!" unless rails
18
+
19
+ frameworks.each { |framework| $:.unshift(File.join(rails, framework, 'lib')) }
20
+ $:.unshift File.join(TEST_ROOT_DIR, '/../lib')
21
+ $:.unshift TEST_ROOT_DIR
22
+
23
+ ENV['RAILS_ENV'] = 'test'
24
+
25
+ # Rails libs
26
+ begin
27
+ require 'active_support'
28
+ require 'active_record'
29
+ require 'action_controller'
30
+ rescue LoadError
31
+ raise "Please install Attachment-San as Rails plugin before running the tests."
32
+ end
33
+
34
+ require "pathname"
35
+ module Rails
36
+ def self.env
37
+ 'test'
38
+ end
39
+
40
+ def self.root
41
+ Pathname.new('/path/to/app')
42
+ end
43
+ end
44
+
45
+ require File.expand_path('../../rails/init', __FILE__)
46
+
47
+ # Libraries for testing
48
+ require 'rubygems' rescue LoadError
49
+ require 'sqlite3'
50
+ require 'test/spec'
51
+ require 'mocha'
52
+ require 'fileutils'
53
+
54
+ logdir = File.join(TEST_ROOT_DIR, 'log')
55
+ FileUtils.mkdir_p(logdir)
56
+ ActiveRecord::Base.logger = Logger.new File.join(logdir, 'test.log')
57
+ ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ":memory:")
58
+
59
+ TMP_DIR = File.join(TEST_ROOT_DIR, 'tmp')
60
+
61
+ # Classes and methods to aid testing
62
+ require 'lib/schema'
63
+
64
+ require 'fixtures/models/attachment'
65
+ require 'fixtures/models/document'
66
+ require 'fixtures/models/options_stub'
@@ -0,0 +1,167 @@
1
+ require File.expand_path('../test_helper', __FILE__)
2
+
3
+ describe "AttachmentSan's variant class methods" do
4
+ it "should return the variant class to use" do
5
+ reflection = Logo.reflect_on_variant(:header)
6
+ reflection[:class].should.be MyVariant
7
+ end
8
+
9
+ it "should by default use the AttachmentSan::Variant class" do
10
+ reflection = Image.reflect_on_variant(:thumbnail)
11
+ reflection[:class].should.be AttachmentSan::Variant
12
+ end
13
+
14
+ it "should not define a variant twice" do
15
+ count_before = Logo.variant_reflections.length
16
+ Document.has_attachment :logo, :variants => { :header => MyVariant }
17
+ Logo.variant_reflections.length.should == count_before
18
+ end
19
+ end
20
+
21
+ describe "A AttachmentSan::Variant instance in general" do
22
+ before do
23
+ @upload = rails_icon
24
+ @document = Document.new
25
+
26
+ @image = @document.images.build(:uploaded_file => @upload)
27
+ @thumbnail = @image.thumbnail
28
+ @medium_sized = @image.medium_sized
29
+ end
30
+
31
+ it "should return the record it is a variant of" do
32
+ @image.original.record.should == @image
33
+ end
34
+
35
+ it "should return the `original' variant" do
36
+ @image.original.should == @image.original
37
+ @thumbnail.original.should == @image.original
38
+ end
39
+
40
+ it "should call the process proc" do
41
+ MyProcessor.expects(:new).with(@thumbnail)
42
+ @thumbnail.process!
43
+ end
44
+
45
+ it "should return the base_path specified in the attachment_san_options" do
46
+ @thumbnail.base_path.should == Attachment.attachment_san_options[:base_path]
47
+ end
48
+
49
+ it "should return the public_base_path specified in the attachment_san_options" do
50
+ @thumbnail.public_base_path.should == Attachment.attachment_san_options[:public_base_path]
51
+ end
52
+
53
+ it "should return the extension of the original file" do
54
+ Image.attachment_san_options[:extension] = :keep_original
55
+ @thumbnail.extension.should == 'png'
56
+ end
57
+
58
+ it "should use the extension specified in the attachment_san_options" do
59
+ Image.attachment_san_options[:extension] = :jpeg
60
+ @thumbnail.extension.should == :jpeg
61
+ end
62
+
63
+ it "should create the directory that the file_path returns" do
64
+ Image.attachment_san_options[:filename_scheme] = :record_identifier
65
+
66
+ @thumbnail.dir_path.should == File.dirname(@thumbnail.file_path)
67
+ File.should.not.exist @thumbnail.dir_path
68
+ @thumbnail.mkdir!
69
+ File.should.exist @thumbnail.dir_path
70
+ end
71
+
72
+ it "should return a file_path without trailing dot if the original filename has no extension" do
73
+ variant = @document.misc_files.build(
74
+ :uploaded_file => uploaded_file(File.expand_path('../../Rakefile', __FILE__), 'text/plain')
75
+ ).original
76
+
77
+ variant.file_path.should == File.join(variant.base_path, 'Rakefile')
78
+ end
79
+
80
+ { :keep_original => 'png', :jpg => 'jpg' }.each do |setting, ext|
81
+ it "should return a filename named after the variant name plus #{setting}" do
82
+ Image.attachment_san_options[:filename_scheme] = :variant_name
83
+ Image.attachment_san_options[:extension] = setting
84
+
85
+ @thumbnail.filename.should == "thumbnail.#{ext}"
86
+ @thumbnail.file_path.should == File.join(@thumbnail.base_path, "thumbnail.#{ext}")
87
+ @thumbnail.public_path.should == File.join(@thumbnail.public_base_path, "thumbnail.#{ext}")
88
+
89
+ @medium_sized.filename.should == "medium_sized.#{ext}"
90
+ @medium_sized.file_path.should == File.join(@thumbnail.base_path, "medium_sized.#{ext}")
91
+ @medium_sized.public_path.should == File.join(@thumbnail.public_base_path, "medium_sized.#{ext}")
92
+ end
93
+
94
+ it "should return a filename which should be a random token from the record and append #{setting}" do
95
+ Image.attachment_san_options[:filename_scheme] = :token
96
+ Image.attachment_san_options[:extension] = setting
97
+ @image.stubs(:token).returns('556d2e8e')
98
+
99
+ @image.original.filename.should == "55/6d/2e/8e/rails.png"
100
+ @image.original.file_path.should == File.join(@thumbnail.base_path, "55/6d/2e/8e/rails.png")
101
+ @image.original.public_path.should == File.join(@thumbnail.public_base_path, "55/6d/2e/8e/rails.png")
102
+
103
+ @thumbnail.filename.should == "55/6d/2e/8e/rails.thumbnail.#{ext}"
104
+ @thumbnail.file_path.should == File.join(@thumbnail.base_path, "55/6d/2e/8e/rails.thumbnail.#{ext}")
105
+ @thumbnail.public_path.should == File.join(@thumbnail.public_base_path, "55/6d/2e/8e/rails.thumbnail.#{ext}")
106
+ end
107
+
108
+ it "should return a filename which is based on the record identifier and variant name plus #{setting}" do
109
+ Attachment.reset!
110
+
111
+ Image.attachment_san_options[:extension] = setting
112
+ Image.attachment_san_options[:filename_scheme] = :record_identifier
113
+ @image.save!
114
+
115
+ @thumbnail.filename.should == "/images/#{@image.id}/thumbnail.#{ext}"
116
+ @thumbnail.file_path.should == File.join(@thumbnail.base_path, "/images/#{@image.id}/thumbnail.#{ext}")
117
+ @thumbnail.public_path.should == File.join(@thumbnail.public_base_path, "/images/#{@image.id}/thumbnail.#{ext}")
118
+
119
+ @medium_sized.filename.should == "/images/#{@image.id}/medium_sized.#{ext}"
120
+ @medium_sized.file_path.should == File.join(@thumbnail.base_path, "/images/#{@image.id}/medium_sized.#{ext}")
121
+ @medium_sized.public_path.should == File.join(@thumbnail.public_base_path, "/images/#{@image.id}/medium_sized.#{ext}")
122
+ end
123
+ end
124
+ end
125
+
126
+ describe "AttachmentSan::Variant, concerning a default variant without extra options" do
127
+ before do
128
+ @upload = rails_icon
129
+ @document = Document.new
130
+ @document.build_watermark :uploaded_file => @upload
131
+ end
132
+
133
+ it "should use a custom variant for an `original'" do
134
+ @document.watermark.original.should.be.instance_of AttachmentSan::Variant::Original
135
+ end
136
+
137
+ it "should simply copy the file to the file_path" do
138
+ Attachment.reset!
139
+
140
+ file_path = @document.watermark.original.file_path
141
+ File.should.not.exist file_path
142
+ @document.save!
143
+
144
+ File.should.exist file_path
145
+ File.read(file_path).should == File.read(@upload.path)
146
+ end
147
+ end
148
+
149
+ describe "AttachmentSan::Variant, concerning a default variant with extra options" do
150
+ before do
151
+ @upload = rails_icon
152
+ @document = Document.new
153
+ @variant = @document.misc_files.build(:uploaded_file => @upload).original
154
+ end
155
+
156
+ it "should return the filename_scheme value that was specified" do
157
+ @variant.filename_scheme.should == :keep_original end
158
+
159
+ it "should return the original file's name" do
160
+ @variant.filename.should == @variant.record.filename
161
+ end
162
+
163
+ it "should call the specified process proc" do
164
+ result = @variant.process!
165
+ result.should == :from_process_proc
166
+ end
167
+ end
metadata ADDED
@@ -0,0 +1,92 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: attachment-san
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Eloy Duran
8
+ - Manfred Stienstra
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2009-11-19 00:00:00 +01:00
14
+ default_executable:
15
+ dependencies: []
16
+
17
+ description: Rails plugin for easy and rich attachment manipulation.
18
+ email:
19
+ - eloy@fngtps.com
20
+ - manfred@fngtps.com
21
+ executables: []
22
+
23
+ extensions: []
24
+
25
+ extra_rdoc_files:
26
+ - LICENSE
27
+ - README
28
+ files:
29
+ - LICENSE
30
+ - README
31
+ - Rakefile
32
+ - TODO
33
+ - VERSION
34
+ - attachment-san.gemspec
35
+ - examples/photo.rb
36
+ - examples/photos_controller.rb
37
+ - examples/post.rb
38
+ - lib/attachment_san.rb
39
+ - lib/attachment_san/has.rb
40
+ - lib/attachment_san/test/helper.rb
41
+ - lib/attachment_san/test/rails.png
42
+ - lib/attachment_san/variant.rb
43
+ - napkin.rb
44
+ - rails/init.rb
45
+ - test/attachment_san_test.rb
46
+ - test/fixtures/models/attachment.rb
47
+ - test/fixtures/models/document.rb
48
+ - test/fixtures/models/options_stub.rb
49
+ - test/has_test.rb
50
+ - test/lib/schema.rb
51
+ - test/test_helper.rb
52
+ - test/variant_test.rb
53
+ has_rdoc: true
54
+ homepage: http://github.com/Fingertips/attachment-san
55
+ licenses: []
56
+
57
+ post_install_message:
58
+ rdoc_options:
59
+ - --charset=UTF-8
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: "0"
67
+ version:
68
+ required_rubygems_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: "0"
73
+ version:
74
+ requirements: []
75
+
76
+ rubyforge_project:
77
+ rubygems_version: 1.3.5
78
+ signing_key:
79
+ specification_version: 3
80
+ summary: Rails plugin for easy and rich attachment manipulation.
81
+ test_files:
82
+ - test/attachment_san_test.rb
83
+ - test/fixtures/models/attachment.rb
84
+ - test/fixtures/models/document.rb
85
+ - test/fixtures/models/options_stub.rb
86
+ - test/has_test.rb
87
+ - test/lib/schema.rb
88
+ - test/test_helper.rb
89
+ - test/variant_test.rb
90
+ - examples/photo.rb
91
+ - examples/photos_controller.rb
92
+ - examples/post.rb