Making attachment_fu polymorphic

I am working on a small model mixin called attachment_kung to make
attachment_fu polymorphic, so you no longer need a different table and
Model class for every associated attachment (Productimage, Ad_doc,
etc). All you really need is one model and table to handel all your
attachments - in some cases, anyway. I have the code working, but have
run into one small hitch that I can’t seem to ferret out: after the
attachment is saved to the db (I am using mySQL 5) and I want to
display a list of the attachments, the order they are returned in is
strange. I say ‘strange’ because I, at least, cannot descern what they
are ORDERed BY. They seem to come back grouped (an image an its
thumbnails will come back adjecent to one another), but the order of
the groups (or even the elements within the groups) is unpredictable.

Any help would be appriciated, my code is below.

Thanks in advance,
Paul Saieg


module Attachment_kung

Attachment_kung: Polymorphic Support For Attachment_fu

Version 0.1

---------------------------------------------------------------

– Kung, a potent concentration of effort –

---------------------------------------------------------------

Originally written by Paul Saieg, 14 September 2007

email me with comments at classicist at gmail dot com

Licensed under a Creative Commons Attribution-ShareAlike 2.5

License

Creative Commons — Attribution-ShareAlike 2.5 Generic — CC BY-SA 2.5

################################### DESCRIPTION
###############################################

Attachment_kung supports a polymorphic use of attachment_fu, so

you no longer need a different table

and Model class for every associated attachment (Productimage,

Ad_doc, etc.). Instead it relies

on a single Model class called ‘Attachment’ and a table called

‘attachments’ (or whatever you want

to call them). It dynamically creates a class called

#{CallingClass}_attachment and then returns an

instance of that class with its polymorphic attributes

(whateverable_type/id) set to match its parent

object. It is called by an object, not a class, because a

polymorphic attachment

must necessarily belong to a particular Model object. DRY up your

life.
################################### USE
#######################################################

To setup Attachment_kung,

1. Create the Attachment_fu model and table (I used Attachment

and attachments),

set them up for polymorphism. Add to the table a column called

path_prefix.

(I used: belongs_to :attachable, :polymorphic => true for the

model and attachable_type and

attachable_id for the table); and configure the Attachment_fu

model with

has_attachment :storage => :file_system

include Attachment_kung

Make sure Attachment_kung is last, or you will not be able

to display your attachments. Also,

I have not tested Attachment_kung with :storage => :s3

or :storage => :db_system. I ought to

work, but one never knows until one tries. Sadly, at present,

Attachment_kung has the limitation

that it can only handle one storage system at a time. Maybe

in a further iteration it will let

one use both the file_system and s3 at the same time. Maybe.

2. Mix in Attachment_kung to whatever Model(s) you want to have

the attachments and configure them as

you normally would for attachment_fu,except use

has_polymorphic_attachment instead of

has_attachment and validates_as_polymorphic_attachment instead

of validates_as_attachment.

Write the configuration params just as you normally would. For

more info on

setting up attachemnt_fu see Mike C.'s excellent tutorial:

http://clarkware.com/cgi/blosxom/2007/02/24

3. Give your Attachment_kung-ed Model the correct has_many for

polymorphism

(I used: has_many :attachments, :as => :attachable);

4. Set the belongs_to and table_name methods to inside

Attachment_kung’s @class_def variable to

match what you’ve done so far (the default is Attachment and

attachments). Also set

the attachables hash in Attachment_kung’s intialize to match

the names of your model’s

polymorphic attrubutes (the defaults are :attachable_id

and :attachable_type). Finally, set the

‘file_system’ case default in in intialize statement to

‘public/#{table_name}’ as per your previous

choice (again, the default is ‘public/attachments’)

5. Install the attachment_fu plugin and double check to make sure

things are set up right.

To use Attachment_kung,

1. In your controller, get a Model with Attachment_kung mixed in

from the database

(@p = Product.find(1), we’ll say)

2. Call the new_attachment method and pass it the params with the

uploaded_data. The line

should look something like this: @attachment =

@p.new_attachment(params[:attachment])

3. Attachments, before they are saved, have a class name of

#{CallingClass}_attachment

(ie if a Product calls new_attachment then the returned

object’s class will be Product_attachment).

Attachments, after they are saved, can be called either with

the Attachment class (or whatever you

created as your Attachment_fu model) or by the usual

polymorphic means. All of the usual methods

are available from Attachment_fu (like public_filename etc.)

For a more on polymorphic Models see Chad F.'s Rails

Recipes.
################################### FAIR WARNING
##############################################

The new_attachment method makes use of EVAL. In case you don’t

know, eval runs whatever string

it gets as ruby code. If you pass eval “rm *” it will ruin

your day. Be very careful about

how you use it, and NEVER EVER open it to your users. Also,

Attachment_kung has not yet been

tested for use with Attachment_fu’s database or s3 strorage.

This software comes with no warranty,

expressed or implied.

#############################################################################################

@@attachment_config = []
@@validates_as_attachment = nil

adds methods to the class that is mixing the module in

def self.append_features(someClass)
@@klass = someClass
def someClass.has_polymorphic_attachment(config)
@@attachment_config = config
end

 def someClass.validates_as_polymorphic_attachment
   @@validates_as_attachment = true
 end
 super

end

may only work for macs. If this is giving you trouble on a PC, try

reversing the direction of the '/'s

this over writes the public_filename method in attachment_fu.

NOTA BENE: this over writes attachment_fu’s public_filename, if

you over write attachment_fu’s

full_filename, you may also have to over write Attachment_kung’s

public_filename.
def public_filename(thumbnail = nil)
if path_prefix != “”
path_prefix.gsub!(‘public’,‘’) if path_prefix.include?
(‘public’)
path_prefix + ‘/’ + (“%08d” %
main_attachment_id).scan(/…/).map{|e|e.to_s + ‘/’}.to_s + filename
else
begin
Technoweenie::AttachmentFu::Backends::S3Backend::s3_url
rescue NoMethodError
raise ‘Attachment has a bad path, please delete it and upload
it again’
end
end
end

gets the attachment’s id, or its parent’s id if it has one

def main_attachment_id
((respond_to?(:parent_id) && parent_id) || id).to_i
end

creates the dynamic attachment class and returns an object of that

type with its polymorphic

attributes set to reflect the caller

def new_attachment(params)
# name of calling class
class_name = self.class.to_s

# id of calling object
parent = self.object_id

# options from has_polymorphic_attachment
options = {}
options.merge!(@@attachment_config)

# whether validate_as_polymorphic_attachment is set in caller

(default is nil)
valid = @@validates_as_attachment

@class_def = %{
  class Object::#{class_name}_attachment < ActiveRecord::Base
    # set polymorphic column names (whateverable_id/type)
    belongs_to :attachable, :polymorphic => true

    # set table into which model will be saved
    self.table_name = "attachments"

    # Initialize polymorphic attributes (attachable_id/type) with

data from parent obj;
# also initialize value of path_prefix column for
public_filename to use.
# NOTE: if you are working with different polymorphic column
names be sure to update them
# from my :attachable_id and :attachable_type to whatever you
are using. Also, be sure to
# update the default value for ‘path’ in the file_system case
from ‘attachements’
# to whatever your table name is.

    def initialize(attributes={})
      @p = ObjectSpace._id2ref(#{parent})
      path = if "#{@@attachment_config[:path_prefix]}".to_s == ""
             pth = case "#{@@attachment_config[:storage]}".to_s
                       when "file_system"
                         "public/attachments"
                       when "db_system"
                         ""
                       when "s3"
                         ""
                       else
                         ""
                 end
             else
               pth = "#{@@attachment_config[:path_prefix]}".to_s
             end
      if respond_to?(:path_prefix)
         attachables = {:attachable_id => @p.id, :attachable_type

=> @p.class.to_s, :path_prefix => path}
else
attachables = {:attachable_id => @p.id, :attachable_type
=> @p.class.to_s}
end
attributes.merge!(attachables)
super(attributes)
end

    # Passes the params from the parent model (passed to

has_polymorphic_attachment) to
# the attachment_fu’s has_attachment. Also runs
attachment_fu’s validations,
# if validates_as_polymorphic_attachment was called by the
calling Model
def self.attach(opts, valid)
has_attachment opts
validates_as_attachment if valid
end

    ##END Class
  end
  }

  eval @class_def

  eval("#{class_name}_attachment").attach(options, valid)
  attachment = eval("#{class_name}_attachment").new(params)
  return attachment

end

END MODULE

end

Here this is version 0.2 of attachment_kung. It has a few bugs worked
out, is a bit faster, but the order of the ‘attachments’ table is
still unpredictable. Any help would be appreciated.

Thanks in advance,
Paul Saieg

#Attachment_kung: Polymorphic Support For Attachment_fu

Version 0.2

Originally written by Paul Saieg, 14 September 2007

email me with comments at classicist at gmail dot com

Licensed under a Creative Commons Attribution-ShareAlike 2.5 License

Creative Commons — Attribution-ShareAlike 2.5 Generic — CC BY-SA 2.5

module Attachment_kung
@@attachment_config = []
@@validates_as_attachment = nil

adds methods to the class that is mixing the module in

def self.append_features(someClass)
@@klass = someClass
def someClass.has_polymorphic_attachment(config)
@@attachment_config = config
end

 def someClass.validates_as_polymorphic_attachment
   @@validates_as_attachment = true
 end
 super

end

Gets the full path to the filename in this format:

# This assumes a model name like MyModel

# public/#{table_name} is the default filesystem path

RAILS_ROOT/public/my_models/5/blah.jpg

The optional thumbnail argument will output the thumbnail’s

filename.
def full_filename(thumbnail = nil)
file_system_path = (thumbnail ? thumbnail_class :
self).path_prefix.to_s
File.join(RAILS_ROOT, file_system_path,
*partitioned_path(thumbnail_name_for(thumbnail)))
end

may only work for macs. If this is giving you trouble on a PC, try

reversing the direction of the ‘/‘s
def public_filename(thumbnail = nil)
if path_prefix != “”
path_prefix.gsub!(‘public’,’’) if path_prefix.include?
(‘public’)
path_prefix + ‘/’ + (“%08d” %
main_attachment_id).scan(/…/).map{|e|e.to_s + ‘/’}.to_s + filename
else
begin
Technoweenie::AttachmentFu::Backends::S3Backend::s3_url
rescue NoMethodError
raise ‘Attachment has a bad path, please delete it and upload
it again’
end
end
end

gets the attachment’s id, or its parent’s id if it has one

def main_attachment_id
((respond_to?(:parent_id) && parent_id) || id).to_i
end

creates the dynamic attachment class and returns an object of that

type with its polymorphic

attributes set to reflect the caller

def new_attachment(params)

# name of calling class
class_name = self.class.to_s

# id of calling object
parent = self.object_id

# options from has_polymorphic_attachment
options = {}
options.merge!(@@attachment_config)

# whether validate_as_polymorphic_attachment is set in caller

(default is nil)
valid = @@validates_as_attachment

@class_def = %{
  class Object::#{class_name}_attachment < ActiveRecord::Base

   belongs_to :attachable, :polymorphic => true
    self.table_name = "attachments"

   # intialize object, only tested with :storage_type

=> :file_system
def initialize(attributes={})
@p = nil
@p = ObjectSpace._id2ref(#{parent})
path = if “#{@@attachment_config[:path_prefix]}”.to_s == “”
“public/attachments”
else
“#{@@attachment_config[:path_prefix]}”.to_s
end
if respond_to?(:path_prefix)
attachables = {:attachable_id => @p.id, :attachable_type
=> @p.class.to_s, :path_prefix => path}
else
attachables = {:attachable_id => @p.id, :attachable_type
=> @p.class.to_s}
end
@p = nil
attributes.merge!(attachables)
super(attributes)
end

    def self.attach(opts, valid)
      has_attachment opts
      validates_as_attachment if valid
    end

    ##END Class
  end
  }

  eval @class_def

  eval("#{class_name}_attachment").attach(options, valid)
  attachment = eval("#{class_name}_attachment").new(params)
  return attachment

end

END MODULE

end

I am pleased to announce that attachment_kung has been tested and
works. The ordering problem had nothing to do with the code, but was
the result of using a myISAM table rather than an InnoDB table. An
InnoDB table is crucial for the ordering to work. Below is the
completed code. Enjoy it, and please let me know if anyone runs into
any problems (I haven’t tested every possible use case).

  • Paul Saieg

module Attachment_kung

Attachment_kung: Polymorphic Support For Attachment_fu

Version 0.3

Originally written by Paul Saieg, 14 September 2007

email me with comments at classicist at gmail dot com

Licensed under a Creative Commons Attribution-ShareAlike 2.5

License

Creative Commons — Attribution-ShareAlike 2.5 Generic — CC BY-SA 2.5

################################### DESCRIPTION
###############################################

Attachment_kung supports a polymorphic use of attachment_fu, so

you no longer need a different table

and Model class for every associated attachment (Productimage,

Ad_doc, etc.). Instead it relies

on a single Model class called ‘Attachment’ and a table called

‘attachments’ (or whatever you want

to call them). It dynamically creates a class called

#{CallingClass}_attachment and then returns an

instance of that class with its polymorphic attributes

(whateverable_type/id) set to match its parent

object. It is called by an object, not a class, because a

polymorphic attachment

must necessarily belong to a particular Model object. DRY up your

life.
################################### USE
#######################################################

To setup Attachment_kung,

1. Create the Attachment_fu model and table (I used Attachment

and attachments),

set them up for polymorphism. If you are using mySQL, make

sure the table is InnoDB

(myISAM will not work). Add to the table a column called

path_prefix.

(I used: belongs_to :attachable, :polymorphic => true for the

model and attachable_type and

attachable_id for the table); and configure the Attachment_fu

model with

has_attachment :storage => :file_system

include Attachment_kung

Make sure Attachment_kung is last, or you will not be able

to display your attachments. Also,

I have not tested Attachment_kung with :storage => :s3

or :storage => :db_system. I ought to

work, but one never knows until one tries. Sadly, at present,

Attachment_kung has the limitation

that it can only handle one storage system at a time. Maybe

in a further iteration it will let

one use both the file_system and s3 at the same time. Maybe.

2. Mix in Attachment_kung to whatever Model(s) you want to have

the attachments and configure them as

you normally would for attachment_fu,except use

has_polymorphic_attachment instead of

has_attachment and validates_as_polymorphic_attachment instead

of validates_as_attachment.

Write the configuration params just as you normally would. For

more info on

setting up attachemnt_fu see Mike C.'s excellent tutorial:

http://clarkware.com/cgi/blosxom/2007/02/24

3. Give your Attachment_kung-ed Model the correct has_many for

polymorphism

(I used: has_many :attachments, :as => :attachable);

4. Set the belongs_to and table_name methods to inside

Attachment_kung’s @class_def variable to

match what you’ve done so far (the default is Attachment and

attachments). Also set

the attachables hash in Attachment_kung’s intialize to match

the names of your model’s

polymorphic attrubutes (the defaults are :attachable_id

and :attachable_type). Finally, set the

‘file_system’ case default in in intialize statement to

‘public/#{table_name}’ as per your previous

choice (again, the default is ‘public/attachments’)

5. Install the attachment_fu plugin and double check to make sure

things are set up right.

To use Attachment_kung,

1. In your controller, get a Model with Attachment_kung mixed in

from the database

(@p = Product.find(1), we’ll say)

2. Call the new_attachment method and pass it the params with the

uploaded_data. The line

should look something like this: @attachment =

@p.new_attachment(params[:attachment])

3. Attachments, before they are saved, have a class name of

#{CallingClass}_attachment

(ie if a Product calls new_attachment then the returned

object’s class will be Product_attachment).

Attachments, after they are saved, can be called either with

the Attachment class (or whatever you

created as your Attachment_fu model) or by the usual

polymorphic means. All of the usual methods

are available from Attachment_fu (like public_filename etc.)

For a more on polymorphic Models see Chad F.'s Rails

Recipes.
################################### FAIR WARNING
##############################################

The new_attachment method makes use of EVAL. In case you don’t

know, eval runs whatever string

it gets as ruby code. If you pass eval “rm *” it will ruin

your day. Be very careful about

how you use it, and NEVER EVER open it to your users. Also,

Attachment_kung has not yet been

tested for use with Attachment_fu’s database or s3 strorage.

This software comes with no warranty,

expressed or implied.

#############################################################################################

@@attachment_config = []
@@validates_as_attachment = nil

adds methods to the class that is mixing the module in

def self.append_features(someClass)
@@klass = someClass
def someClass.has_polymorphic_attachment(config)
@@attachment_config = config
end

 def someClass.validates_as_polymorphic_attachment
   @@validates_as_attachment = true
 end
 super

end

Gets the full path to the filename in this format:

# This assumes a model name like MyModel

# public/#{table_name} is the default filesystem path

RAILS_ROOT/public/my_models/5/blah.jpg

The optional thumbnail argument will output the thumbnail’s

filename.
def full_filename(thumbnail = nil)
file_system_path = (thumbnail ? thumbnail_class :
self).path_prefix.to_s
File.join(RAILS_ROOT, file_system_path,
*partitioned_path(thumbnail_name_for(thumbnail)))
end

May only work for macs. If this is giving you trouble on a PC,

try

reversing the direction of the '/'s

def public_filename(thumbnail = nil)
if path_prefix != “”
path_prefix.gsub!(‘public’,‘’) if path_prefix.include?
(‘public’)
path_prefix + ‘/’ + (“%08d” %
main_attachment_id).scan(/…/).map{|e|e.to_s + ‘/’}.to_s + filename
else
begin
Technoweenie::AttachmentFu::Backends::S3Backend::s3_url
rescue NoMethodError
raise ‘Attachment has a bad path, please delete it and upload
it again’
end
end
end

gets the attachment’s id, or its parent’s id if it has one

def main_attachment_id
((respond_to?(:parent_id) && parent_id) || id).to_i
end

creates the dynamic attachment class and returns an object of that

type with its polymorphic

attributes set to reflect the caller

def new_attachment(params)

# name of calling class
class_name = self.class.to_s

# id of calling object
parent = self.object_id

# options from has_polymorphic_attachment
options = {}
options.merge!(@@attachment_config)

# whether validate_as_polymorphic_attachment is set in caller

(default is nil)
valid = @@validates_as_attachment

@class_def = %{
  class Object::#{class_name}_attachment < ActiveRecord::Base
    # set polymorphic column names (whateverable_id/type)
    belongs_to :attachable, :polymorphic => true

    # set table into which model will be saved
    self.table_name = "attachments"

    # Initialize polymorphic attributes (attachable_id/type) with

data from parent obj;
# also initialize value of path_prefix column for
public_filename to use.
# NOTE: if you are working with different polymorphic column
names be sure to update them
# from my :attachable_id and :attachable_type to whatever you
are using. Also, be sure to
# update the default value for ‘path’ in the file_system case
from ‘attachements’
# to whatever your table name is.

    def initialize(attributes={})
      @p = nil
      @p = ObjectSpace._id2ref(#{parent})
      path = if "#{@@attachment_config[:path_prefix]}".to_s == ""
             "public/attachments"
             else
              "#{@@attachment_config[:path_prefix]}".to_s
             end
      if respond_to?(:path_prefix)
         attachables = {:attachable_id => @p.id, :attachable_type

=> @p.class.to_s, :path_prefix => path}
else
attachables = {:attachable_id => @p.id, :attachable_type
=> @p.class.to_s}
end
@p = nil
attributes.merge!(attachables)
super(attributes)
end

    # passes attachment_fu it's options hash and runs its

validations
def self.attach(opts, valid)
has_attachment opts
validates_as_attachment if valid
end

    ##END Class
  end
  }

  eval @class_def

  eval("#{class_name}_attachment").attach(options, valid)
  attachment = eval("#{class_name}_attachment").new(params)
  return attachment

end

END MODULE

end