Topic: Sanity Check - Polymorphic things

Hi all,

Quick sanity check if I may so I don't have to undo too much later on.

I have an application that will be primarily driven by xml inputs, but loosely it consists of registered users (users), whom submit proposals (proposals). Each proposal has one applicant(applicants) and maybe one or none joint applicant(joint_applicants).

An applicant is a *person* and has many addresses; other things will follow, like employers

Applicant and joint_applicant tables are near identical, both have first and last names etc. Now for ease I initally fleshed out a working app using two seperate but near identical tables, but duplicating what are effectively people records didn't quite sit right with me.

So I've added a people table and linked it in to applicant and joint_applicant, they are afterall just people in a proposal.

What I have now, logially works on my head and in code, I'm just not certain it's the right way to do it.

So some code to clarify the above.

class Proposal < ActiveRecord::Base
belongs_to :user

has_one :applicant
has_one :joint_applicant
validates_presence_of  :user_id #, :proposal_type
end

class Applicant < ActiveRecord::Base

has_many :addresses, :as => :addressable, :order => ("end_date")
has_one :person, :as => :personable
end

class JointApplicant < ActiveRecord::Base

has_many :addresses, :as => :addressable, :order => ("end_date")
has_one :person, :as => :personable
end

class Address < ActiveRecord::Base

#belongs_to :applicant
belongs_to :addressable, :polymorphic => true
end
class Person < ActiveRecord::Base
    belongs_to :personable, :polymorphic => true
end


Ok, so I can now add person data to an applicant object and so on. But Applicant now is a hollow table, everything that pertianed to an applicant is actually personal so it's in the people table.

My xml submits (which can be changed) are made up of, as I code it I reuse the applicant array to invisilby create the person data:

 @person = @applicant.build_person(params[:applicant] || params[:request][:applicant]) if (params[:applicant] || params[:request][:applicant])
<request>
  <applicant><first_name>Curly</first_name><last_name>Jones</last_name><gender>male</gender></applicant>

<jointapplicant><first_name>Curletta</first_name><last_name>Jones</last_name><gender>female<gender></jointapplicant>

</request>


Now this seems to me to be for the API user an easy way to use it, for the code layer; well it feels ok, but I have this phantom table of applicant and of course jointapplicant, which now are no more than filters for the people.

So would it be better to:
* Keep the above
* Add an is_applicant or is_jointapplicant boolean field to people and update the xml to just pass person data and an appropriate flag.
* Use single table inheritance from the api docs on activerecordbase whereby my applicant = firm in the example given http://api.rubyonrails.org/classes/Acti … /Base.html so

class Person < ActiveRecord::Base; end
  class Applicant < Person; end

Then drop all the applicant controller and simply have Applicant.create in the proposal controller?

* your suggestions wink

Sorry for the long post, I hoped I might be able to answer it myself by time I'd typed it out. I'm sure there are a few ways of doing this, my sanity check is finding the right way from a beginners point of view and lots of quieries does not feel quite right...

Thanks.

Last edited by colindensem (2007-02-14 07:58:05)

Re: Sanity Check - Polymorphic things

Can a given person be only an Applicant or Joint Applicant? Or can he be both? If he can be both, you wouldn't want to use single table inheritance.

I'm not too familiar with the problem domain, but it sounds like how a person is determined to be an applicant/joint applicant is how he is related to a proposal. In that case you can do something really simple like this:

class Proposal < ActiveRecord::Base
  belongs_to :applicant, :class_name => 'Person', :foreign_key => 'applicant_id'
  belongs_to :joint_applicant, :class_name => 'Person', :foreign_key => 'joint_applicant_id'
end

class Person < ActiveRecord::Base
  has_many :proposals_as_applicant, :class_name => 'Proposal', :foreign_key => 'applicant_id'
  has_many :proposals_as_joint_applicant, :class_name => 'Proposal', :foreign_key => 'joint_applicant_id'
  has_many :addresses
end

class Address < ActiveRecord::Base
  belongs_to :person
end


This way you aren't juggling empty models around. Not sure if this schema provides enough flexibility though.

Railscasts - Free Ruby on Rails Screencasts

Re: Sanity Check - Polymorphic things

A user owns the proposal, a user is an authorised submitter of data from sources, e.g. website or desktop app. They create and own proposals.

A Proposal consists of the applicant and none or one joint_applicant.

Each applicant / joint applicant has addresses.

An applicant and joint applicant are both people (now).

A person cannot be an applicant and joint applicant on the same proposal, but he/she may be present many times as an applicant or joint applicant, but it will be for a new proposal. It is very possible that the same person applies as an applicant and as a joint applicant, both are valid in this business. The reporting side will pick up duplicates and rouge elements.

At least thats how the story goes on my pad; the story is basically a loan application, but many loans (proposals) covering a wide range of things, web forms are completed and a proposal is generated, to this are added the applicant, joint; if present and address data etc.

I'm going to chew over your suggestion in a quick new test project and see how the applicant owning a proposal goes... I'd not thought of it that way. SWMBO will be pleased now.

From the info given, do you think I've gone in roughly the right direction or have I got the wrong end of the ruby stick? I'm a little confused now, so I'm going to sit back and get a clean sheet of paper out.

My present iteration gives a plausible working solution, if not a little fudge tastic:

Curl Request:
$ curl -H "Accept: application/xml" -H 'Content-Type: application/xml' -u admin:tester -d "<request><applicant><status>curl</status><person><first_name>Curl</first_name><last_name>jones</last_name><
email_address>1@1.com</email_address></person></applicant></request>" http://host:3000/p
roposals/1/applicants


Applicants Controller - Create
def create
person = (params[:request][:applicant].delete(:person) || params[:person])
   
@applicant = @proposal.build_applicant(params[:applicant] || params[:request][:applicant])

#Set up person data in people table.
@person = @applicant.build_person(person)
   
respond_to do |format|

      if @applicant.valid? && @person.valid? #&& @address.valid?
          @applicant.save
          @person.save
     
        flash[:notice] = 'Applicant was successfully created.'
    format.html { redirect_to applicant_url(@proposal,@applicant) }
        format.xml { render :xml => @applicant.to_xml(:include=>[:person, :addresses], :dasherize=>false)  }
      else
       
        format.html { render :action => "new" }
       
        if @applicant.errors.size > 0
        format.xml  { render :xml => @applicant.errors.to_xml}
    else
        format.xml  { render :xml => @person.errors.to_xml}
    end
      end
    end
  end


Result of processing:
<?xml version="1.0" encoding="UTF-8"?>
<applicant>
  <created_at type="datetime">2007-02-14T17:40:11Z</created_at>
  <id type="integer">20</id>
  <proposal_id type="integer">1</proposal_id>
  <status>curl</status>
  <updated_at type="datetime">2007-02-14T17:40:11Z</updated_at>
  <person>
    <additional_income type="integer"></additional_income>
    <additional_income_source></additional_income_source>
    <bank_name></bank_name>
    <bank_start type="date"></bank_start>
    <contact_time></contact_time>
    <created_at type="datetime">2007-02-14T17:40:11Z</created_at>
    <date_of_birth type="datetime"></date_of_birth>
    <debt_payments type="float"></debt_payments>
    <debt_total type="float"></debt_total>
    <dependants type="integer"></dependants>
    <email_address>1@1.com</email_address>
    <employment_status></employment_status>
    <first_name>Curl</first_name>
    <gender></gender>
    <id type="integer">1</id>
    <income type="integer"></income>
    <income_period></income_period>
    <last_name>jones</last_name>
    <maritial_status></maritial_status>
    <middle_name></middle_name>
    <mothers_maidenname></mothers_maidenname>
    <occupation></occupation>
    <personable_id type="integer">20</personable_id>
    <personable_type>Applicant</personable_type>
    <proof_of_income></proof_of_income>
    <property_status></property_status>
    <telephone_home></telephone_home>
    <telephone_mobile></telephone_mobile>
    <telephone_work></telephone_work>
    <title></title>
    <updated_at type="datetime">2007-02-14T17:40:11Z</updated_at>
  </person>
</applicant>

And that extends to process addresses in much the same way thanks to your earlier comment on stripping out the xml bits, much appreciated for that.

And that populats my people table, applicant table, which is nigh on empty, but logically exists as the applicant.

I'm also not happy with the kludge to spit out errors to xml, but thats for another day.

Last edited by colindensem (2007-02-14 13:56:05)

Re: Sanity Check - Polymorphic things

colindensem wrote:

A person cannot be an applicant and joint applicant on the same proposal, but he/she may be present many times as an applicant or joint applicant, but it will be for a new proposal. It is very possible that the same person applies as an applicant and as a joint applicant, both are valid in this business. The reporting side will pick up duplicates and rouge elements.

This confirms my theory. What makes a person an applicant/joint applicant is how he relates to a proposal. A person can be either one. If you need to prevent a person from being both in the same proposal, this can be done with simple validation.

colindensem wrote:

I'm going to chew over your suggestion in a quick new test project and see how the applicant owning a proposal goes... I'd not thought of it that way. SWMBO will be pleased now.

The User can still own a proposal (belongs_to User), my previous suggestion was just a way you can manage the people without the join models (Applicant and JointApplicant). Since the join models had no data then it makes sense to remove them. They still may need their own logic, but you may be able to get by with putting that in the Person or Proposal classes.

Instead of thinking about creating an Applicant/JointApplicant, think about creating a person and assigning him to the proposal. Of course your interface can present it differently.

Edit: Also, don't be confused by the belongs_to lingo. This just means the foreign key is in the "proposals" table (applicant_id). It doesn't mean the person owns or manages the proposal model.

Railscasts - Free Ruby on Rails Screencasts

Re: Sanity Check - Polymorphic things

Thanks Ryan,

Slept on this and I'm going to have to give it a go.

The thing I'm not sure on is the person having many proposals. They won't as I'm not in an ideal world.

For example, they the person might use n different websites which are submitting to us. As such that person, although a single physical human being, is to us, n seperate people for data capture with n seperate proposals. Just as he's seen n different websites we need to see n different entities.

So each person can only ever be on one proposal, although he/she maybe duplicated in the database it will always be associated to another proposal and more importantly the data owner (user), who may just to make things easier have several sources of data capture, so potentially a person could be replicated many times over, but we have to track this and swallow the duplicated data pill.

I've really benefitted from this thread, it's made me look at everything again in a different way. As I'd previously been thinking create a proposal and then add the data elements, so everything is driven from a proposal id and not from the person perspective, which is what's important.

As a foot note to this, previously I used before_filters to obtain the user and then the proposal, which gave me some secuirty to data, that is you could only view the proposal for the given data owner.
Just working through the people controller which now based on above, has proposals. I wish to prevent users who don't own a proposal being able to view people records not associated to there account or even the proposal. Previosuly done by way of @user.proposal.applicant(:id) and nested routes on proposal.

This does not hold true anymore given is' applicant>proposal, so I've done the following, which works, except not for delete so back to the drawing board, but again have I gone off track? If not then I'll crack on smile

def find_proposal(proposal_id = false || params[:proposal_id] || params[:id])
  @user = User.find_by_id(current_user)
  return @proposal = @user.proposals.find(proposal_id)
end


Person Controller
before_filter :find_proposal
def show
@person = Person.find_by_id(params[:id],
:joins=> 'inner join proposals on proposals.applicant_id = people.id or proposals.joint_applicant_id = people.id',
:conditions=> ['proposals.id = ?', @proposal.id])

Last edited by colindensem (2007-02-15 08:41:10)

Re: Sanity Check - Polymorphic things

colindensem wrote:

For example, they the person might use n different websites which are submitting to us. As such that person, although a single physical human being, is to us, n seperate people for data capture with n seperate proposals. Just as he's seen n different websites we need to see n different entities.

So each person can only ever be on one proposal, although he/she maybe duplicated in the database it will always be associated to another proposal and more importantly the data owner (user), who may just to make things easier have several sources of data capture, so potentially a person could be replicated many times over, but we have to track this and swallow the duplicated data pill.

Oh, okay. I misunderstood your previous post. You could use a has_one association in Person. You can even add a method to help fetch the proper proposal. For example:

class Person < ActiveRecord::Base
  has_one :proposal_as_applicant, :class_name => 'Proposal', :foreign_key => 'applicant_id'
  has_one :proposal_as_joint_applicant, :class_name => 'Proposal', :foreign_key => 'joint_applicant_id'
  has_many :addresses
 
  def proposal
    proposal_as_applicant || proposal_as_joint_applicant
  end
end

colindensem wrote:

As a foot note to this, previously I used before_filters to obtain the user and then the proposal, which gave me some secuirty to data, that is you could only view the proposal for the given data owner.
Just working through the people controller which now based on above, has proposals. I wish to prevent users who don't own a proposal being able to view people records not associated to there account or even the proposal. Previosuly done by way of @user.proposal.applicant(:id) and nested routes on proposal.

This does not hold true anymore given is' applicant>proposal, so I've done the following, which works, except not for delete so back to the drawing board, but again have I gone off track? If not then I'll crack on smile

Not really that much has changed so I'm not certain of the problem. The schema is now simpler than what you had before, so the code shouldn't be more complex.

From my understanding you need to add permission handling to only display the person if the user owns the proposal that the person manages? If you have a "proposal" method in the Person model as shown above you can do this:

@person = Person.find(params[:id])
if @person.proposal.user == @user
  # user owns proposal
else
  # user does not own proposal, raise an error
end

Does that work for you?

Railscasts - Free Ruby on Rails Screencasts

Re: Sanity Check - Polymorphic things

ryanb wrote:

Not really that much has changed so I'm not certain of the problem. The schema is now simpler than what you had before, so the code shouldn't be more complex.

From my understanding you need to add permission handling to only display the person if the user owns the proposal that the person manages? If you have a "proposal" method in the Person model as shown above you can do this:

@person = Person.find(params[:id])
if @person.proposal.user == @user
  # user owns proposal
else
  # user does not own proposal, raise an error
end

Does that work for you?

Yep, I'd missed the fact that the proposal(s)_as_applicant meant that person.proposal didn't work, you added the final bit of the puzzle above with that def proposal in the person model, it all fell together then big_smile

A nice way of doing things indeed, glad I asked as I knew the first cut was not *right*.

Next step, review rest of what I have now smile

Last edited by colindensem (2007-02-16 03:40:28)