attachment-san 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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