Commit 110df00f by jyurek

Extensive documentation, adding ability to crop thumbnails.

git-svn-id: https://svn.thoughtbot.com/plugins/paperclip/trunk@171 7bbfaf0e-4d1d-0410-9690-a8bb5f8ef2aa
parent 41d7fdea
Paperclip
=========
=Paperclip
Paperclip is a lightweight attachment manager for ActiveRecord. It saves and manages your attachments, be they images or Word Docs, with one line of code. You can automatically thumbnail images as they're uploaded, and you don't have to worry about installing any ruby-specific libraries. You don't have to worry about compiling headaches with RMagick, concurrency issues and race conditions with MiniMagick, or unsupported image types with ImageScience. All you need is a working Image- or GraphicsMagick installation -- the convert command is all you need.
Paperclip is a lightweight attachment manager for ActiveRecord. It saves and manages your attachments, be they images or Word Docs, with one line of code. You can automatically thumbnail images as they're uploaded, and you don't have to worry about installing any ruby-specific libraries. You don't have to worry about compiling headaches with RMagick, concurrency issues and race conditions with MiniMagick, or unsupported image types with ImageScience. All you need is a working Image- or GraphicsMagick installation -- the +convert+ command is all you need.
Paperclip uses the filesystem to save your files. You specify a root that the files will be saved to, and, if you're attaching images, any other formats they need to be converted to, and they'll all be saved to the right place when your object saves. They can even validate beforehand, so you'll know if your user tries to upload an HTML doc as an image.
Paperclip uses the filesystem to save your files. You specify a root that the files will be saved to, and, if you're attaching images, any other sizes they need to be converted to, and they'll all be saved to the right place when your object saves.
Usage
-----
See the documentation for the +has_attached_file+ method for extensive details.
==Usage
In your model:
class Photo < ActiveRecord::Base
class Photo < ActiveRecord::Base
has_attached_file :image, :thumbnails => { :medium => "300x300>", :thumb => "100x100>" }
end
end
In your edit and new views:
<% form_for :photo, @photo, :url => photo_path do |form| %>
<% form_for :photo, @photo, :url => photo_path, :html => { :multipart => true } do |form| %>
<%= form.file_field :image %>
<% end %>
<% end %>
In your controller:
def create
def create
@photo = Photo.create( params[:photo] )
end
end
In your show view:
<%= image_tag @photo.image_url %>
<%= image_tag @photo.image_url(:original) %>
<%= image_tag @photo.image_url(:medium) %>
<%= image_tag @photo.image_url(:thumb) %>
<%= image_tag @photo.image_url %>
<%= image_tag @photo.image_url(:original) %>
<%= image_tag @photo.image_url(:medium) %>
<%= image_tag @photo.image_url(:thumb) %>
......@@ -7,16 +7,25 @@ task :default => :test
desc 'Test the paperclip plugin.'
Rake::TestTask.new(:test) do |t|
t.libs << 'lib'
t.libs << 'lib' << 'profile'
t.pattern = 'test/**/*_test.rb'
t.verbose = true
end
desc 'Generate documentation for the paperclip plugin.'
Rake::RDocTask.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.rdoc_dir = 'doc'
rdoc.title = 'Paperclip'
rdoc.options << '--line-numbers' << '--inline-source'
rdoc.rdoc_files.include('README')
rdoc.rdoc_files.include('lib/**/*.rb')
end
desc 'Clean up files.'
task :clean do |t|
FileUtils.rm_rf "doc"
FileUtils.rm_rf "repository"
FileUtils.rm_rf "tmp"
FileUtils.rm "test/debug.log" rescue nil
FileUtils.rm "test/paperclip.db" rescue nil
end
\ No newline at end of file
require File.join(File.dirname(__FILE__), "lib", "paperclip")
ActiveRecord::Base.extend( Thoughtbot::Paperclip::ClassMethods )
File.send :include, Thoughtbot::Paperclip::Upfile
\ No newline at end of file
module Thoughtbot
# Paperclip allows file attachments that are stored in the filesystem. All graphical
# transformations are done using the Graphics/ImageMagick command line utilities and
# are stored in-memory until the record is saved. Paperclip does not require a
# separate model for storing the attachment's information, and it only requires two
# columns per attachment.
#
# Author:: Jon Yurek
# Copyright:: Copyright (c) 2007 thoughtbot, inc.
# License:: Distrbutes under the same terms as Ruby
#
# See the +has_attached_file+ documentation for more details.
module Thoughtbot #:nodoc:
# Paperclip defines an attachment as any file, though it makes special considerations
# for image files. You can declare that a model has an attached file with the
# +has_attached_file+ method:
#
# class User < ActiveRecord::Base
# has_attached_file :avatar, :thumbnails => { :thumb => "100x100" }
# end
#
# See the +has_attached_file+ documentation for more details.
module Paperclip
DEFAULT_OPTIONS = {
:path_prefix => ":rails_root/public/",
:url_prefix => "/",
:path => ":class/:style/:id",
PAPERCLIP_OPTIONS = {
:whiny_deletes => false,
:whiny_thumbnails => true
}
def self.options
PAPERCLIP_OPTIONS
end
DEFAULT_ATTACHMENT_OPTIONS = {
:path_prefix => ":rails_root/public",
:url_prefix => "",
:path => ":class/:id/:style_:name",
:attachment_type => :image,
:thumbnails => {}
:thumbnails => {},
:delete_on_destroy => true
}
class ThumbnailCreationError < StandardError; end #:nodoc
class ThumbnailDeletionError < StandardError; end #:nodoc
module ClassMethods
def has_attached_file *file_attribute_names
options = file_attribute_names.last.is_a?(Hash) ? file_attribute_names.pop : {}
options = DEFAULT_OPTIONS.merge(options)
# == Methods
# +has_attached_file+ attaches a file (or files) with a given name to a model. It creates seven instance
# methods using the attachment name (where "attachment" in the following is the name
# passed in to +has_attached_file+):
# * attachment: Returns the name of the file that was attached, with no path information.
# * attachment?: Alias for _attachment_ for clarity in determining if the attachment exists.
# * attachment=(file): Sets the attachment to the file and creates the thumbnails (if necessary).
# +file+ can be anything normally accepted as an upload (+StringIO+ or +Tempfile+) or a +File+
# if it has had the +Upfile+ module included.
# Note this does not save the attachments.
# user.avatar = File.new("~/pictures/me.png")
# user.avatar = params[:user][:avatar] # When :avatar is a file_field
# * attachment_file_name(style = :original): The name of the file, including path information. Pass in the
# name of a thumbnail to get the path to that thumbnail.
# user.avatar_file_name(:thumb) # => "public/users/44/thumb/me.png"
# user.avatar_file_name # => "public/users/44/original/me.png"
# * attachment_url(style = :original): The public URL of the attachment, suitable for passing to +image_tag+
# or +link_to+. Pass in the name of a thumbnail to get the url to that thumbnail.
# user.avatar_url(:thumb) # => "http://assethost.com/users/44/thumb/me.png"
# user.avatar_url # => "http://assethost.com/users/44/original/me.png"
# * attachment_valid?: If unsaved, returns true if all thumbnails have data (that is,
# they were successfully made). If saved, returns true if all expected files exist and are
# of nonzero size.
# * destroy_attachment(complain = false): Deletes the attachment and all thumbnails. Sets the +attachment_file_name+
# column and +attachment_content_type+ column to +nil+. Set +complain+ to true to override
# the +whiny_deletes+ option.
#
# == Options
# There are a number of options you can set to change the behavior of Paperclip.
# * +path_prefix+: The location of the repository of attachments on disk. See Interpolation below
# for more control over where the files are located.
# :path_prefix => ":rails_root/public"
# :path_prefix => "/var/app/repository"
# * +url_prefix+: The root URL of where the attachment is publically accessible. See Interpolation below
# for more control over where the files are located.
# :url_prefix => "/"
# :url_prefix => "/user_files"
# :url_prefix => "http://some.other.host/stuff"
# * +path+: Where the files are stored underneath the +path_prefix+ directory and underneath the +url_prefix+ URL.
# See Interpolation below for more control over where the files are located.
# :path => ":class/:style/:id/:name" # => "users/original/13/picture.gif"
# * +attachment_type+: If this is set to :image (which it is, by default), Paperclip will attempt to make thumbnails.
# * +thumbnails+: A hash of thumbnail styles and their geometries. You can find more about geometry strings
# at the ImageMagick website (http://www.imagemagick.org/script/command-line-options.php#resize). Paperclip
# also adds the "#" option, which will resize the image to fit maximally inside the dimensions and then crop
# the rest off (weighted at the center).
# * +delete_on_destroy+: When records are deleted, the attachment that goes with it is also deleted. Set
# this to +false+ to prevent the file from being deleted.
#
# == Interpolation
# The +path_prefix+, +url_prefix+, and +path+ options can have dynamic interpolation done so that the
# locations of the files can vary depending on a variety of factors. Each variable looks like a Ruby symbol
# and is searched for with +gsub+, so a variety of effects can be achieved. The list of possible variables
# follows:
# * +rails_root+: The value of the +RAILS_ROOT+ constant for your app. Typically used when putting your
# attachments into the public directory. Probably not useful in the +path+ definition.
# * +class+: The underscored, pluralized version of the class in which the attachment is defined.
# * +attachment+: The pluralized name of the attachment as given to +has_attached_file+
# * +style+: The name of the thumbnail style for the current thumbnail. If no style is given, "original" is used.
# * +id+: The record's id.
# * +name+: The file's name, as stored in the attachment_file_name column.
#
# When interpolating, you are not confined to making any one of these into its own directory. This is
# perfectly valid:
# :path => ":attachment/:style/:id-:name" # => "avatars/thumb/44-me.png"
#
# == Model Requirements
# For any given attachment _foo_, the model the attachment is in needs to have both a +foo_file_name+
# and +foo_content_type+ column, as a type of +string+. The +foo_file_name+ column contains only the name
# of the file and none of the path information. However, the +foo_file_name+ column accessor is overwritten
# by the one (defined above) which returns the full path to whichever style thumbnail is passed in.
# In a pinch, you can either use +read_attribute+ or the plain +foo+ accessor, which returns the database's
# +foo_file_name+ column.
#
# == Event Triggers
# When an attachment is set by using he setter (+model.attachment=+), the thumbnails are created and held in
# memory. They are not saved until the +after_save+ trigger fires, at which point the attachment and all
# thumbnails are written to disk.
#
# Attached files are destroyed when the associated record is destroyed in a +before_destroy+ trigger. Set
# the +delete_on_destroy+ option to +false+ to prevent this behavior. Also note that using the ActiveRecord's
# +delete+ method instead of the +destroy+ method will prevent the +before_destroy+ trigger from firing.
def has_attached_file *attachment_names
options = attachment_names.last.is_a?(Hash) ? attachment_names.pop : {}
options = DEFAULT_ATTACHMENT_OPTIONS.merge(options)
include InstanceMethods
attachments ||= {}
file_attribute_names.each do |attr|
attachment_names.each do |attr|
attachments[attr] = (attachments[attr] || {:name => attr}).merge(options)
define_method "#{attr}=" do |uploaded_file|
......@@ -41,21 +157,23 @@ module Thoughtbot
end
define_method attr do
self.send("#{attr}_file_name")
read_attribute("#{attr}_file_name")
end
alias_method "#{attr}?", attr
define_method "#{attr}_attachment" do
attachments[attr]
end
private "#{attr}_attachment"
define_method "#{attr}_filename" do |*args|
define_method "#{attr}_file_name" do |*args|
style = args.shift || :original # This prevents arity warnings
self["#{attr}_file_name"] ? path_for(attachments[attr], style) : ""
read_attribute("#{attr}_file_name") ? path_for(attachments[attr], style) : ""
end
define_method "#{attr}_url" do |*args|
style = args.shift || :original # This prevents arity warnings
self["#{attr}_file_name"] ? url_for(attachments[attr], style) : ""
read_attribute("#{attr}_file_name") ? url_for(attachments[attr], style) : ""
end
define_method "#{attr}_valid?" do
......@@ -66,9 +184,10 @@ module Thoughtbot
end
end
define_method "destroy_#{attr}" do
define_method "destroy_#{attr}" do |*args|
complain = args.first || false
if attachments[attr].keys.any?
delete_attachment attachments[attr]
delete_attachment attachments[attr], complain
end
end
......@@ -84,7 +203,7 @@ module Thoughtbot
define_method "#{attr}_before_destroy" do
if attachments[attr].keys.any?
delete_attachment attachments[attr]
delete_attachment attachments[attr] if attachments[attr][:delete_on_destroy]
end
end
private :"#{attr}_before_destroy"
......@@ -93,26 +212,32 @@ module Thoughtbot
end
end
module InstanceMethods
module InstanceMethods #:nodoc:
private
def path_for attachment, style = :original
prefix = File.join(attachment[:path_prefix], attachment[:path])
def interpolate attachment, prefix_type, style
returning "#{attachment[prefix_type]}/#{attachment[:path]}" do |prefix|
prefix.gsub!(/:rails_root/, RAILS_ROOT)
prefix.gsub!(/:id/, self.id.to_s) if self.id
prefix.gsub!(/:class/, self.class.to_s.underscore)
prefix.gsub!(/:class/, self.class.to_s.underscore.pluralize)
prefix.gsub!(/:style/, style.to_s)
File.join( prefix.split("/"), read_attribute("#{attachment[:name]}_file_name").to_s )
prefix.gsub!(/:attachment/, attachment[:name].to_s.pluralize)
prefix.gsub!(/:name/, attachment[:filename])
end
end
def path_for attachment, style = :original
file = read_attribute("#{attachment[:name]}_file_name")
return nil unless file
prefix = interpolate attachment, :path_prefix, style
File.join( prefix.split("/").reject(&:blank?) )
end
def url_for attachment, style = :original
prefix = File.join(attachment[:url_prefix], attachment[:path])
prefix.gsub!(/:rails_root/, RAILS_ROOT)
prefix.gsub!(/:id/, self.id.to_s) if self.id
prefix.gsub!(/:class/, self.class.to_s.underscore)
prefix.gsub!(/:style/, style.to_s)
File.join( prefix.split("/"), read_attribute("#{attachment[:name]}_file_name").to_s )
file = read_attribute("#{attachment[:name]}_file_name")
return nil unless file
interpolate attachment, :url_prefix, style
end
def ensure_directories_for attachment
......@@ -132,13 +257,13 @@ module Thoughtbot
end
end
def delete_attachment attachment
def delete_attachment attachment, complain = false
(attachment[:thumbnails].keys + [:original]).each do |style|
file_path = path_for(attachment, style)
begin
FileUtils.rm(file_path)
rescue Errno::ENOENT
raise if ::Thoughtbot::Paperclip.whiny_deletes?
raise ThumbnailDeletionError if ::Thoughtbot::Paperclip.options[:whiny_deletes] || complain
end
end
self.update_attribute "#{attachment[:name]}_file_name", nil
......@@ -146,18 +271,58 @@ module Thoughtbot
end
def make_thumbnail orig_io, geometry
thumb = IO.popen("convert - -scale '#{geometry}' -", "w+") do |io|
operator = geometry[-1,1]
geometry, crop_geometry = geometry_for_crop(geometry, orig_io) if operator == '#'
command = "convert - -scale '#{geometry}' #{operator == '#' ? "-crop '#{crop_geometry}'" : ""} -"
thumb = IO.popen(command, "w+") do |io|
orig_io.rewind
io.write(orig_io.read)
io.close_write
io.read
StringIO.new(io.read)
end
if ::Thoughtbot::Paperclip.options[:whiny_thumbnails]
raise ThumbnailCreationError, "Convert returned with result code #{$?.exitstatus}." unless $?.success?
end
thumb
end
def geometry_for_crop geometry, orig_io
IO.popen("identify -", "w+") do |io|
orig_io.rewind
io.write(orig_io.read)
io.close_write
if match = io.read.split[2].match(/(\d+)x(\d+)/)
src = match[1,2].map(&:to_f)
srch = src[0] > src[1]
dst = geometry.match(/(\d+)x(\d+)/)[1,2].map(&:to_f)
dsth = dst[0] > dst[1]
ar = src[0] / src[1]
scale_geometry, scale = if dst[0] == dst[1]
if srch
[ "x#{dst[1]}", src[1] / dst[1] ]
else
[ "#{dst[0]}x", src[0] / dst[0] ]
end
elsif dsth
[ "#{dst[0]}x", src[0] / dst[0] ]
else
[ "x#{dst[1]}", src[1] / dst[1] ]
end
crop_geometry = if dsth
"%dx%d+%d+%d" % [ dst[0], dst[1], 0, (src[1] / scale - dst[1]) / 2 ]
else
"%dx%d+%d+%d" % [ dst[0], dst[1], (src[0] / scale - dst[0]) / 2, 0 ]
end
[ scale_geometry, crop_geometry ]
end
end
raise "Convert returned with result code #{$?.exitstatus}." unless $?.success?
StringIO.new(thumb)
end
def is_a_file? data
[:size, :content_type, :original_filename].map do |meth|
[:size, :content_type, :original_filename, :read].map do |meth|
data.respond_to? meth
end.all?
end
......@@ -168,29 +333,29 @@ module Thoughtbot
protected :sanitize_filename
end
# The Upfile module is a convenience module for adding uploaded-file-type methods
# to the +File+ class. Useful for testing.
# user.avatar = File.new("test/test_avatar.jpg")
module Upfile
# Infer the MIME-type of the file from the extension.
def content_type
type = self.path.match(/\.(\w+)$/)[1]
case type
when "jpg", "png", "gif" then "image/#{type}"
when "txt", "csv", "xml", "html", "htm" then "text/#{type}"
else "application/#{type}"
else "x-application/#{type}"
end
end
# Returns the file's normal name.
def original_filename
self.path
end
# Returns the size of the file.
def size
File.size(self)
end
end
def self.whiny_deletes?
false
end
end
end
File.send :include, Thoughtbot::Paperclip::Upfile
......@@ -7,16 +7,35 @@ begin
table.column :document_file_name, :string
table.column :document_content_type, :string
end
ActiveRecord::Base.connection.create_table :non_standards do |table|
table.column :resume_file_name, :string
table.column :resume_content_type, :string
table.column :avatar_file_name, :string
table.column :avatar_content_type, :string
end
rescue Exception
end
class Foo < ActiveRecord::Base
has_attached_file :image, :attachment_type => :image,
:thumbnails => { :thumb => "100x100>", :medium => "300x300>" },
:path_prefix => "."
:path_prefix => "./repository"
end
class Bar < ActiveRecord::Base
has_attached_file :document, :attachment_type => :document,
:path_prefix => "."
:path_prefix => "./repository"
end
class NonStandard < ActiveRecord::Base
has_attached_file :resume, :attachment_type => :document,
:path_prefix => "/tmp",
:path => ":attachment_:id_:name"
has_attached_file :avatar, :attachment_type => :image,
:thumbnails => { :cropped => "200x10#",
:bigger => "1000x1000",
:smaller => "200x200>",
:square => "150x150#" },
:path_prefix => "./repository",
:path => ":class/:attachment/:id/:style_:name"
end
\ No newline at end of file
......@@ -17,15 +17,15 @@ class PaperclipImagesTest < Test::Unit::TestCase
def test_should_save_the_file_and_its_thumbnails
assert @foo.save
assert File.exists?( @foo.image_filename(:original) ), @foo.image_filename(:original)
assert File.exists?( @foo.image_filename(:medium) ), @foo.image_filename(:medium)
assert File.exists?( @foo.image_filename(:thumb) ), @foo.image_filename(:thumb)
assert File.size?( @foo.image_filename(:original) )
assert File.size?( @foo.image_filename(:medium) )
assert File.size?( @foo.image_filename(:thumb) )
out = `identify '#{@foo.image_filename(:original)}'`; assert out.match("405x375"); assert $?.exitstatus == 0
out = `identify '#{@foo.image_filename(:medium)}'`; assert out.match("300x278"); assert $?.exitstatus == 0
out = `identify '#{@foo.image_filename(:thumb)}'`; assert out.match("100x93"); assert $?.exitstatus == 0
assert File.exists?( @foo.image_file_name(:original) ), @foo.image_file_name(:original)
assert File.exists?( @foo.image_file_name(:medium) ), @foo.image_file_name(:medium)
assert File.exists?( @foo.image_file_name(:thumb) ), @foo.image_file_name(:thumb)
assert File.size?( @foo.image_file_name(:original) )
assert File.size?( @foo.image_file_name(:medium) )
assert File.size?( @foo.image_file_name(:thumb) )
out = `identify '#{@foo.image_file_name(:original)}'`; assert out.match("405x375"); assert $?.exitstatus == 0
out = `identify '#{@foo.image_file_name(:medium)}'`; assert out.match("300x278"); assert $?.exitstatus == 0
out = `identify '#{@foo.image_file_name(:thumb)}'`; assert out.match("100x93"); assert $?.exitstatus == 0
end
def test_should_validate_to_make_sure_the_thumbnails_exist
......@@ -36,7 +36,7 @@ class PaperclipImagesTest < Test::Unit::TestCase
def test_should_delete_all_thumbnails_on_destroy
assert @foo.save
names = [:original, :medium, :thumb].map{|style| @foo.image_filename(style) }
names = [:original, :medium, :thumb].map{|style| @foo.image_file_name(style) }
assert @foo.destroy
names.each {|path| assert !File.exists?( path ), path }
end
......
require 'test/unit'
require File.dirname(__FILE__) + "/test_helper.rb"
require File.dirname(__FILE__) + "/../init.rb"
require File.join(File.dirname(__FILE__), "models.rb")
class PaperclipNonStandardTest < Test::Unit::TestCase
def setup
assert @ns = NonStandard.new
assert @resume = File.new(File.join(File.dirname(__FILE__), 'fixtures', 'test_document.doc'))
assert @avatar = File.new(File.join(File.dirname(__FILE__), 'fixtures', 'test_image.jpg'))
assert @ns.resume = @resume
assert @ns.avatar = @avatar
end
def test_should_validate_before_save
assert @ns.avatar_valid?
assert @ns.valid?
end
def test_should_save_the_created_file_to_the_final_asset_directory
assert @ns.save
assert File.exists?( @ns.resume_file_name )
assert File.exists?( @ns.avatar_file_name(:original) )
assert File.exists?( @ns.avatar_file_name(:bigger) )
assert File.exists?( @ns.avatar_file_name(:cropped) )
assert File.size?( @ns.avatar_file_name(:original) )
assert File.size?( @ns.avatar_file_name(:bigger) )
assert File.size?( @ns.avatar_file_name(:cropped) )
out = `identify '#{@ns.avatar_file_name(:original)}'`; assert_match /405x375/, out, out; assert $?.exitstatus == 0
out = `identify '#{@ns.avatar_file_name(:bigger)}'`; assert_match /1000x926/, out, out; assert $?.exitstatus == 0
out = `identify '#{@ns.avatar_file_name(:cropped)}'`; assert_match /200x10/, out, out; assert $?.exitstatus == 0
end
def test_should_validate
assert @ns.save
assert @ns.resume_valid?
assert @ns.avatar_valid?
assert @ns.valid?
end
def test_should_default_to_original_for_path_and_url
assert_equal @ns.resume_file_name(:original), @ns.resume_file_name
assert_equal @ns.resume_url(:original), @ns.resume_url
assert_equal @ns.avatar_file_name(:original), @ns.avatar_file_name
assert_equal @ns.avatar_url(:original), @ns.avatar_url
end
def test_should_delete_files_on_destroy
assert @ns.save
assert File.exists?( @ns.resume_file_name ), @ns.resume_file_name
assert File.exists?( @ns.avatar_file_name(:original) )
assert File.exists?( @ns.avatar_file_name(:bigger) )
assert File.exists?( @ns.avatar_file_name(:cropped) )
resume_file_name = @ns.resume_file_name
avatar_file_names = [:original, :bigger, :cropped].map{|style| @ns.avatar_file_name(style) }
assert @ns.destroy
assert !File.exists?( resume_file_name ), resume_file_name
avatar_file_names.each do |name|
assert !File.exists?(name), name
end
end
end
\ No newline at end of file
......@@ -17,7 +17,7 @@ class PaperclipTest < Test::Unit::TestCase
def test_should_save_the_created_file_to_the_final_asset_directory
assert @bar.save
assert File.exists?( @bar.document_filename )
assert File.exists?( @bar.document_file_name )
end
def test_should_validate
......@@ -27,17 +27,17 @@ class PaperclipTest < Test::Unit::TestCase
end
def test_should_default_to_original_for_path_and_url
assert_equal @bar.document_filename(:original), @bar.document_filename
assert_equal @bar.document_file_name(:original), @bar.document_file_name
assert_equal @bar.document_url(:original), @bar.document_url
end
def test_should_delete_files_on_destroy
assert @bar.save
assert File.exists?( @bar.document_filename ), @bar.document_filename
assert File.exists?( @bar.document_file_name ), @bar.document_file_name
document_filename = @bar.document_filename
document_file_name = @bar.document_file_name
assert @bar.destroy
assert !File.exists?( document_filename ), document_filename
assert !File.exists?( document_file_name ), document_file_name
end
end
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment