Solving polymorphic has_one through building and nested forms
April 4th 2012I recently hit on the brilliant idea of using STI (single table inheritence), polymorphic classes, nested forms and has_one through models all at once just to prove how silly I am.
My use case is an asset manager backed by paperclip. I wanted all assets to go into a single S3 folder and be mananged by a single :assets table. Then I wanted any other model to be able to associate uploaded assets at any arity. Gumpf!
My base class for an STI compliant asset table is
# == Schema Information
#
# Table name: assets
#
# id :integer not null, primary key
# attachment_file_name :string(255)
# attachment_content_type :string(255)
# attachment_file_size :integer
# attachment_updated_at :datetime
# created_at :datetime not null
# updated_at :datetime not null
# type :string(255)
# attachment_processing :boolean
# terminated :boolean default(FALSE)
#
class Asset < ActiveRecord::Base
belongs_to :assetable, :polymorphic => true
delegate :url, :to => :attachment
delegate :expiring_url, :to => :attachment
delegate :present?, :to => :attachment
validates_attachment_presence :attachment
has_many :assetable_assets, :dependent => :destroy
has_many :assetables, :through => :assetable_assets, :as => :assetable
AssetPath = ":rails_env/assets/:id/:style.:extension"
def self.configure_attachment options = {}
options.merge! :path => AssetPath
has_attached_file :attachment, options
end
end
Then I need a class to associate other objects, assetables, with uploaded assets. Note that assetables are polymorphic
# == Schema Information
#
# Table name: assetable_assets
#
# id :integer not null, primary key
# assetable_id :integer
# assetable_type :string(255)
# asset_id :integer
# created_at :datetime not null
# updated_at :datetime not null
#
class AssetableAsset < ActiveRecord::Base
belongs_to :assetable, :polymorphic => true
belongs_to :asset
# Clean up orphans
before_destroy do
if not asset.nil?
if asset.assetable_assets.count == 1
asset.background_destroy
end
end
end
end
And an example of a concrete assetable class.
# == Schema Information
#
# Table name: users
#
# id :integer not null, primary key
# email :string(255) default(""), not null
#
class User < ActiveRecord::Base
has_one :assetable_asset, :as => :assetable, :dependent => :destroy
has_one :avatar, :source => :asset, :through => :assetable_asset, :class_name => "ImageAsset"
attr_accessible :avatar_attributes
accepts_nested_attributes_for :avatar
def build_avatar attributes={}, opts={}
asset = ::ImageAsset.new attributes, opts
build_assetable_asset :asset => asset
self.avatar = asset
end
def avatar_with_build
a = avatar_without_build
unless a
return build_avatar
end
a
end
alias_method_chain :avatar, :build
end
Now I can upload an avatar via a formtastic form as so
= semantic_form_for @user, :url => user_profile_path(@user), :html => { :multipart => true } do |f|
= f.inputs do
.span5
= f.input :email
= f.input :name
- if user.avater.present?
= image_tag user.avatar.url(:thumb)
= f.inputs :attachment, :for => :avatar do |a|
= a.input :attachment, :as => :file
= f.buttons
For some reason that I can’t get my head around Rails doesn’t ship with a handler for polymorphic has_one builders. I expected to be able to do
user.build_avatar
out of the box but I got an error that the method does not exist and it took some fiddling to figure out how to emulate it.
For reference, The image asset class is defined below which fleshes out the STI part of the problem. Maybe it would not be necessary to have STI support on the assets table but it might happen that I have assets with different styles and need to classify them as such.
class ImageAsset < Asset
configure_attachment styles: ImageSizes.standard_sizes_hash
validates_attachment_content_type :attachment,
:content_type => %r{image/.*},
:less_than => 5.megabyte
end