Topic: DRYing up redundant controllers, models, etc - the polymorphic assoc.

So I've continued to stride forward with my app and have gotten to the point of reducing the number of different image tables & controllers, etc. in my app, and rolling it into one big happy unified table, replete with exciting polymorphic associations.

For reference, I used the recipe book's recipe #23 - "Polymorphic Associations - Has_Many :whatevers" (p.109-113).

The basic structure and command line stuff worked like a charm. (I can run through the stuff in the console as demonstrated in the recipe and everything works, everything saves, etc.)

However, I'm missing something here when I'm trying to translate this to the real world.

Rules, Objects, Sites, and Galleries all can have Images.
so, model code:

#in rules, objects, sites, galleries...
has_many :images, :as=>:imageable

#in images
belongs_to :imageable, :polymorphic=>true


So I'm trying to do a form for uploading images that allows me to pass the relevant info (stored in the database as imageable_id, imageable_type), and this is where it's not seeming to work...

#_form.rhtml partial:
<%= error_messages_for 'image' %>

<!--[form:image]-->
<p><label for="image_title">Title</label><br/>
<%= text_field 'image', 'title'  %></p>

<p><label for="image_caption">Caption</label><br/>
<%= text_area 'image', 'caption'  %></p>

<p><label for="image_image"> Choose Image File: </label><%= file_field("ruleimage", "image")%><br/><br/>
    <%= hidden_field :image, :created_by, :value => session[:admin] %></p>
    <%= hidden_field :image, :imageable_type, :value=>@refobject.type%>
    <%= hidden_field :image, :imageable_id, :value=>@refobject.id%>
  <%= submit_tag "Add Image" %>

<!--[eoform:image]-->
#images_controller.rb
  def new
    @image = Image.new
    @refobject = Rule.find(:all, :conditions => ['id = ?', '99']) ## this is a dummy value until I work out the paths here.
  end

  def create
    @image = Image.new(params[:image])
    if @image.save
      flash[:notice] = 'Image was successfully created.'
      redirect_to :action => 'list'
    else
      render :action => 'new'
    end
  end

#image.rb

class Image < ActiveRecord::Base
 
    DIRECTORY = 'public/support_images'

    belongs_to :imageable, :polymorphic=>true
    after_save :process
    after_destroy :cleanup

    def image=(image)
        if image == nil or image == ""
        else
          @image = image
          write_attribute 'extension', image.original_filename.split('.').last.downcase
      end
    end
   
    def imageable=(imageable)
      write_attribute 'imageable_id', imageable.id
      write_attribute 'imageable_type', "rule"
    end
#from this point forward, it's just the same code from the recipe and from other controllers that work that I'm attempting to DRY up..
    def fullsize_path
       File.join(DIRECTORY, "#{self.imageable_type}_#{self.id}_#{self.timestamp}.#{extension}")
    end

    private

    def process
      if @image
        create_directory
        cleanup
        save_fullsize
        @image = nil
      end
    end

    def save_fullsize
      File.open(fullsize_path,'wb') do |file|
        file.puts @image.read
      end
    end

    def create_directory
      FileUtils.mkdir_p DIRECTORY
    end

    def cleanup
      Dir[File.join(DIRECTORY, "#{self.imageable_type}_#{self.id}_#{self.timestamp}.#{extension}")].each do |filename|
        File.unlink(filename) rescue nil
      end 
    end
end


When I run this, it gives a weird (incorrect) value for imageable_type and imageable_id in the DB. In this case, for one I just ran:
imageable_id = 19251268
imageable_type = Array

The latter I understand why it says Array (since the Rule.find... in the controller returns it into @refobject, which is an array). I would assume 19251268 is a valid number that is cropping up for the same reason. Thinking this may function like the console example in the recipe, I tried changing @refobject to refobject in both the controller and the _form partial, and that died with the error "undefined local variable or method `refobject'".

Then I tried @refobject[0].id and @refobject[0].type, which did give me the right values. But it doesn't seem like it's right for the One True Rails Way.. nor, really, does putting all of this in the view -- seems like the view should just point a reference to the object and the model should pop out all the values it needs to put in the DB.

(As one last note to this long-winded post.. I tried changing the

write_attribute 'imageable_type', "rule"
#to the following:
write_attribute 'imageable_type', imageable.type ## or imageable.class

and I get the message "can't dump anonymous class Class").

Any help for me? I really want to do this the right way, because if I'm going to use RoR it's stupid to hack it back to being PHP.

Re: DRYing up redundant controllers, models, etc - the polymorphic assoc.

OK... I think I'm one step closer but I don't feel like it's any cleaner.

#images controller 

  def new
    id = '2' #test value
    target=Rule #test value
    getVars(target, id)
  end

  def getVars(target, id)
    @refobject = target.find(:all, :conditions=>["id = ?", id])
    @reftype = @refobject[0].type
    @refid = @refobject[0].id
  end

#form
    <%= hidden_field :image, :imageable_type, :value=>@reftype%>
        <%= hidden_field :image, :imageable_id, :value=>@refid%>


This works but the catch at this point is that the table name to be queried (target in this example) must exist as the full name of the target table, e.g. Rule, and not a string, e.g. 'Rule'. Is there a way to make this conversion, or am I going to have to do this as a huge lookup table?

I really feel like I'm going about this completely wrong.


EDIT:
Not as much now in the morning.. I kind of like the lookup table method as it allowed me to provide some decent protection and error messaging.

Last edited by Meijin (2006-11-15 15:07:54)

Re: DRYing up redundant controllers, models, etc - the polymorphic assoc.

Normally, in the "new" action you set up the model in memory with the default values. You can set the imageable_type/id here.

def new
  @image = Image.new
  @image.imageable_type = 'Rule' # test value
  @image.imageable_id = '2' # test value
end

This object isn't saved to the database here, it is just loaded into memory so the helper methods can set their default values. Like this:

<%= hidden_field :image, :imageable_type %>
<%= hidden_field :image, :imageable_id %>

Notice we aren't setting the values manually. The helpers do everything for us.

Railscasts - Free Ruby on Rails Screencasts