views:

767

answers:

4

This is something I've been stuck on for a while now, and I have to apologize in advance for going into so much detail for such a simple problem. I just want to make it clear what I'm trying to do here.

Scenario

So, there's a model Foo, each Foo can either be red, green, or blue. Having URLs like /reds to list all red objects, and /reds/some-red-object to show a certain object. In that "show" view, there should be next/previous links, that would essentially "find the next RedFoo in alphabetical order, and once at the last RedFoo, the next record should be the first GreenFoo, continuing in alphabetical order, and so on".

I've tried implementing this in a couple of ways and mostly ended up at a roadblock somewhere. I did get it working for the most part with single table inheritance though, having something like this:

class Foo < ActiveRecord::Base
class RedFoo < Foo
class GreenFoo < Foo
class BlueFoo < Foo

Each subclass's models and controllers are identical, just replace the model names. So the controllers look something like:

class RedFoosController < ApplicationController
  def index
    @foos = RedFoo.find(:all, :order => "title ASC")

    respond_to do |format|
      format.html { render :template => 'foos/index'}
      format.xml  { render :xml => @foos }
    end
  end

  def show
    @foo = RedFoo.find(params[:id])

    respond_to do |format|
      format.html { render :template => 'foos/show'}
      format.xml  { render :xml => @foo }
    end
  end

  def new
    @foo = RedFoo.new

    respond_to do |format|
      format.html { render :template => 'foos/new'}
      format.xml  { render :xml => @foo }
    end
  end

  def edit
    @foo = RedFoo.find(params[:id])

    respond_to do |format|
      format.html { render :template => 'foos/edit'}
    end
  end

  def create
    @foo = RedFoo.new(params[:foo])

    respond_to do |format|
      if @foo.save
        flash[:notice] = 'Foo was successfully created.'
        format.html { redirect_to(@foo) }
        format.xml  { render :xml => @foo, :status => :created, :location => @foo }
      else
        format.html { render :action => "new" }
        format.xml  { render :xml => @foo.errors, :status => :unprocessable_entity }
      end
    end
  end

  def update
    @foo = RedFoo.find(params[:id])

    respond_to do |format|
      if @foo.update_attributes(params[:foo])
        flash[:notice] = 'Foo was successfully updated.'
        format.html { redirect_to(@foo) }
        format.xml  { head :ok }
      else
        format.html { render :action => "edit" }
        format.xml  { render :xml => @foo.errors, :status => :unprocessable_entity }
      end
    end
  end

  def destroy
    @foo = RedFoo.find(params[:id])
    @foo.destroy

    respond_to do |format|
      format.html { redirect_to(foos_url) }
      format.xml  { head :ok }
    end
  end
end

The models only contain methods for next/previous, which work fine, surprisingly.

class RedFoo < Foo
  def next
    if self == RedFoo.find(:all, :order => "title ASC").last
      GreenFoo.find(:all, :order => "title ASC").first
    else
      RedFoo.find(:first, :conditions => ["title > ?", self.title], :order => "title ASC")
    end
  end

  def previous
    if self == RedFoo.find(:all, :order => "title ASC").first
      BlueFoo.find(:all, :order => "title ASC").last
    else
      RedFoo.find(:first, :conditions => ["title < ?", self.title], :order => "title DESC")
    end
  end
end

Problem

For whatever reason when I try to create and edit records, none of the attributes get saved in the database. It simply adds a new record with completely empty columns, regardless of what's filled in the form. No errors get returned in the script/server output or in the log files. From the script/console however, everything works perfectly fine. I can create new records and update their attributes no problem.

It's also quite a bad code smell that I have a lot of code duplication in my controllers/models (they're using the same views as the base model, so that's fine though). But I think that's unavoidable here unless I use some meta-goodness.

Any advice or suggestions about tackling this record saving issue would be great, but the reason I posted my setup in detail is because I have a feeling I'm probably going about this whole thing the wrong way. So, I'm open to other approaches if you know of something more practical than using STI. Thanks.

Update

The parameters hash looks about right:

{"commit"=>"Create", "authenticity_token"=>"+aOA6bBSrZP2B6jsDMnKTU+DIAIkhc8fqoSicVxRJls=", "red_foo"=>{"title"=>"Hello world!"}}

But @foo.inspect returns the following RedFoo object (all nil, except for type):

#<RedFoo id: nil, title: nil, type: "RedFoo", created_at: nil, updated_at: nil>
+1  A: 

Please take a look at the section called "Single table inheritance" on this page and let us know if it solves your problem.

Luca
Err, did you even read the whole question? I'm already using STI, but it presents some problems that i've outlined above.
Mike Richards
I had, then I recreated the scenario you described following the page has been pointed to you, having no issues.I'd check how the variable are named from view to controller (params[...]) to be sure they are mapped in the right way.
Luca
@Luca You're absolutely right, silly mistake on my part. I needed params[:red_foo] in the controller. Didn't think to change this since all the views have the same form_for. Thanks.
Mike Richards
A: 

Must admit, the way I go about STI is to use set_table_name inside a model.

e.g.

class RedFoo < AR::Base
  set_table_name "foos"
  include FooModule
  extend FooClassModule # for self methods

  def next; ...; end
end

But anyway, for this situation, what does your logger say when you do a @foo.inspect just before a save, and also what is the SQL that is ran on insert/update?

Omar Qureshi
I couldn't find anything from @foo.inspect. The SQL just places NULL values in place of "title" and "type", but "created_at" and "updated_at" are getting proper values.
Mike Richards
A: 

Right, so @foo.inspect gives you "nil" in the log?

What I mean (if I wasn't clear enough) was:

def create
@foo = RedFoo.new(params[:foo])
logger.error "******************* foo: #{@foo.inspect} **************"
respond_to do |format|  
  if @foo.save
  ...

if you do that and tail -f your log you can easily find out what is happening to foo and compare that to the incoming params hash

Infact, that would also be some useful information to have, what is the params hash?

Omar Qureshi
Alright, so I included those in the question. Btw, I tried remaking this from scratch, and it works if i create an entire scaffold for each subclass with their own views and everything (which are all identical). Having duplicates of controllers/models is enough bad smell for me, but doing the same with the views is just something I'd never do. Is this how single table inheritance is supposed to work?
Mike Richards
+1  A: 

Problem is the params

:red_foo

is the name of the params in the view, whereas you use

params[:foo]

in the controller, I think the best way would be to be use :foo, in the view by using text_field_tag rather than any (what i assume can be) form builders text_field.

You can get out of the controller smell by using a module to do the basic crud stuff, since i assume most of the new/create/edit/update/destroy stuff is the same

OR

you could map all the routes to a foo controller and use some sort of parameter either passed in from the route, or through URI analysis to get the red/green/blue foo

Omar Qureshi
Thanks, that solution works well here. I'll be trying your suggestion on cleaning up the controller, constantizing the params to get the right class in the base foo controller.
Mike Richards