Thom says:

Warning! Unlike earlier example solutions, there are some significant differences of opinion on how to best implement this ‘extra credit’ question.

You may find yourself with an edited schema that breaks when you try to migrate, and won’t roll back easily, either (I ended up just dropping the whole database a few times until I got it the way I wanted it.)

You may also end up trying to decide whether to still use a foreign key constraint or not (I did), whether to use the strings (cc, po, etc) or just the numeric id’s to store the payment types in their own table (I chose the latter).

And you may run into an infuriatingly easy-seeming problem where if you DO include a prompt for the user (eg, ‘pick a value below’), it is not displayed, or if you prompt => nil, it does show the prompt but always displays the prompt, instead of the value you chose (if one of your other fields fails validation.)

You may then find yourself on an odyssey through the very difficult to access Rails API documentation (where do I find form.select? With examples?) because while the authors kindly list the four parameters to form.select (:attribute, choices, options, html_options) on p485 of the 2nd printed edition… they do not provide any examples of the use of the latter two parameters.

It is Thom’s fervent wish that the “Daves” (authors) would please revisit this section and replace this entire page with a version that is in keeping with the otherwise very well thought out examples on the rest of this wiki. Or that SOMEONE would.

Having been warned(!!) read on… but plan to possibly spend a lot more time trying to make this bit work than many of the previous ones.

Nicolas says:

My first step is to create the model

depot> ruby script/generate model payment_type
Then I edit the migration script 007_create_payment_types.rb

class CreatePaymentTypes < ActiveRecord::Migration
  def self.up
    create_table :payment_types, :options => 'ENGINE=InnoDB DEFAULT CHARSET=utf8' do |t|
      t.column :label, :string, :null => false
      t.column :value, :string, :null => false
    end

    PaymentType.create(:label => 'Check' ,          :value => "check")
    PaymentType.create(:label => 'Credit Card' ,    :value => "cc")
    PaymentType.create(:label => 'Purchase Order' , :value => "po")

    remove_column :orders, :pay_type

    add_column :orders, :payment_type_id, :integer, {:null => false}

    execute "alter table orders add constraint fk_order_payment_types " <<
            "foreign key (payment_type_id) references payment_types(id)" 
  end

  def self.down
    execute "alter table orders drop foreign key fk_order_payment_types" 
    remove_column :orders, :payment_type_id
    add_column :orders, :pay_type, :string, :limit => 10
    drop_table :payment_types
  end
end
Then I edit checkout.rhtml and modify the form.select bit so it pulls its info out of the payment_types table instead of the instance variable:

<%=
form.select :payment_type_id,
    PaymentType.find(:all).collect {|type| [type.label, type.value]},
    :prompt => "-- Select --",
    :selected => nil
%>

Editing order.rb to reflect internal DB changes:


class Order < ActiveRecord::Base
  has_many :line_items

  validates_presence_of :name, :address, :email, :payment_type_id
  validates_inclusion_of :payment_type_id, :in => PaymentType.find_all.map {|type| type.value}

  def add_line_items_from_cart(cart)
    cart.items.each do |item|
      li = LineItem.from_cart_item(item)
      line_items << li
    end
  end
end

I think that covers everything… Correct me otherwise :-)

However, the :prompt option doesn’t seem to work, I’m not sure how to do that at this stage.

Dave:

Maybe because you’re giving the payment_type_id column a default value? :prompt only does its thing if the column has no value.

Nicolas:

You’re correct, I’ve edited my listings above, removing the default value and adding the :selected symbol, set to nil. It works great now.

Ok there is a problem, and I can’t find the answer. The validates_inclusion_of :payment_type_id never returns true. It seems that the value I have put for :in is incorrect, although it does return an array. Any hints?

Nicolas:

I found the problem. I’m giving the drop-down the old values (cc, po etc..) instead of the payment_type_id that is used as a reference. So in checkout.rhtml, I corrected with this:

<%=
form.select :payment_type_id,
    PaymentType.find_all.collect {|type| [type.label, type.id]},
    :prompt => "-- Select --",
    :selected => nil
%>
And in order.rb:

 validates_inclusion_of :payment_type_id, :in => PaymentType.find_all.map {|type| type.id}

And it works fine :-)

I assumed I had to do rake db:migrate, but it failed on the foreign key creation:

execute(“alter table orders add constraint fk_order_payment_types foreign key (payment_type_id) references payment_types(id)”) rake aborted! Mysql::Error: Cannot add or update a child row: a foreign key constraint fails: alter table orders add constraint fk_order_payment_types foreign key (payment_type_id) references payment_types(id) “

I have MySQL? Ver 14.7 Distrib 4.1.20 and looking around in PhpMyAdmin?, I have an orders table, with a payment_type_id field and I have a payment_types table and both tables have “id” as the primary key.

I even tried running this statement manually: “alter table orders add constraint fk_order_payment_types foreign key (payment_type_id) references payment_types(id)” but it fails with the same error.

Does anyone have any clues as to why this is failing for me?

Thanks in advance.

Tiago White:

Hi Nicolas, I am having problems with its solution aproach, when i hit the “checkout”, i’m reciving this error:


 NoMethodError in Store#checkout

Showing app/views/store/checkout.rhtml where line #21 raised:

undefined method `payment_type_id' for #<Order:0x51e1d38>

Extracted source (around line #21):

18:             <p>
19:                 <label for="order_pay_type">Pay with:</label>
20:                 <%=
21:                     form.select :payment_type_id,
22:                     PaymentType.find_all.collect {|type| [type.label, type.id]},
23:                     :prompt => "-- Select --",
24:                     :selected => nil

RAILS_ROOT: ./script/../config/..

BillyGray:

(re foreign key failure) It sounds like you already have existing data in the payment_type_id field that doesn’t match a payment_type in the payment_types table. When you can’t create an FK, it’s usually because existing data violates the proposed constraint. I hope that helps!

Another tip I’ve picked up: when doing numerous updates to your database in a migration, it can be very helpful to wrap it all in a transaction block:

def self.up
  transaction do
    add_index :some_table, :fk_id
    execute "ALTER TABLE some_table ADD CONSTRAINT fk_other_table_id FOREIGN KEY....etc" 
  end
end
This way, if one of your later statements fails, you aren’t left with a mess of a migration to fix and an incorrect schema version in the schema_info table.

However, some databases like MySQL? don’t allow DDL statements like ‘create table’ within a transaction. Which is why you should use PostgreSQL? =P

JimJamesAZ:

I finally got it to take after emptying the line_items and the orders tables.

NicolasConnault:

You may also find it useful to add the option :force => true to your migrations’ table creations, this will add the DROP IF EXISTS statement to the SQL. And of course, if you are using referential integrity, don’t forget to set the table to INNODB


create_table :orders, :force => true, :options => 'ENGINE=InnoDB DEFAULT CHARSET=utf8' do |t|

JimJamesAZ:

I decided it would be best to figure out how I would make the change if I wanted to preserve existing data in the order table. This is what I came up with :

I did not remove the pay_type column until I had populated the payment_type_id column with info from the pay_type column. Here is the relevent portion:


########  begin Portion of 007_create_payment_types.rb#########################
# ... 
PaymentType.create(:label => 'Purchase Order' , :value => "po")      
add_column :orders, :payment_type_id, :integer, {:null => false}
execute "update orders set payment_type_id = '1' where pay_type = 'check'" 
execute "update orders set payment_type_id = '2' where pay_type = 'cc'" 
execute "update orders set payment_type_id = '3' where pay_type = 'po'"      
remove_column :orders, :pay_type
execute "alter table orders add constraint fk_order_payment_types " <<
         "foreign key (payment_type_id) references payment_types(id)" 
# ...
########  end Portion of 007_create_payment_types.rb#########################

NicolasConnault:

Good point, it’s important to consider existing data when applying migrations. However, it’s doubtful that you would have to go back that far on a production application, since this is all part of the initial setup. These kinds of considerations would become more important as the project becomes more mature, with lots of testing happening.

Jim, contact me at some stage when you’re doing the AJAX stuff, I’m having some difficulties, but also discovering some very interesting things.

Jim Alateras:

I ended up using the has_one decorator in Order to define the relationship between order and payment type. I also removed the associated validates_inclusion_of decorator from the class. Is there any disadvantage in doing this?

linoj says:

I dont understand the need for the added complexity of a foreign key here. It seems to make it more fragile (and using the id is an extra level of indirection). For example, if you change the table with more or less options, change their order, and the id’s change, existing data will break. I’d just continue to store the selection list value in the field.

thus, my migration (007_create_paytypes.rb) contains


def self.up
  create_table :paytypes do |t|
    t.column :name, :string, :null => false
    t.column :value, :string, :null => false
  end

  Paytype.delete_all # only needed if creates are in a separate migration
  Paytype.create(:name => 'Check', :value => "check")
  Paytype.create(:name => 'Credit card', :value => "cc")
  Paytype.create(:name => 'Purchase order', :value => "po")
end
checkout.rhtml contains

<%=
form.select :pay_type,
            Paytype.find(:all).map { |p| [ p.name, p.value ] },
            :prompt => "Select a payment method" 
%>

and order.rb changes to


validates_inclusion_of :pay_type, :in => Paytype.find(:all).map { |p| p.value }

ilari says:

If want to use the existing values in the pay_type-field in orders-table, you can use the following solution:

Create the model and migration for pay_types with

script/generate model pay_type
Then modify generated 007_create_pay_types.rb

class CreatePayTypes < ActiveRecord::Migration
 def self.up
   create_table :pay_types, {:id => false} do |t|
     t.column :typeid, :string, :null => false, :limit => 10
     t.column :name, :string, :null => false
   end

   execute "alter table pay_types add primary key (typeid)" 

#    PayType.create(:id => "check",  :name => "Mail Check")
#    PayType.create(:id => "credit", :name => "Credit card")
#    PayType.create(:id => "order",  :name => "Purchase order")

    paytype = PayType.new
    paytype.id = "check" 
    paytype.name = "Mail check" 
    paytype.save

    paytype = PayType.new
    paytype.id = "cc" 
    paytype.name = "Credit card" 
    paytype.save

    paytype = PayType.new
    paytype.id = "po" 
    paytype.name = "Purchase order" 
    paytype.save

   execute "alter table orders add constraint fk_orders_pay_types \
     foreign key (pay_type) references pay_types(typeid)" 
 end

 def self.down
   execute "alter table orders drop foreign key fk_orders_pay_types" 
   drop_table :pay_types
 end
end

Then modify generated pay_type.rb


class PayType < ActiveRecord::Base
  set_primary_key :typeid
end

Then modify order.rb


 validates_inclusion_of :pay_type, :in => PayType.find(:all).map {|p| p.typeid}

And finally modify checkout.rhtml


<p><label for="order_pay_type">Pay with:</label>
<%= form.select :pay_type, PayType.find(:all).map {|p| [p.name, p.typeid]},
  :prompt => "Select a payment method" %></p>

This should get you going, and you are also able to rollback your changes. In the migration script, I could not get the PayType?.creates to work (left in commented). Anyone have a solution for that?

Dave Moore says:

Your create statement is using :id but your column name is :typeid.

Hi there, I just wondered what we should need two values for in the payment_types table… once I moved the payment types into the db I only set up a table with one “name” column (besides the id). I guess that output should always be human readable and internaly I only need an ID. My validation then looks like this:


validates_inclusion_of :payment_type_id, :in => PaymentType.find(:all).collect { |pt| pt.id }

Any reason for having something more then a numeric id?

Anthony says:

I like the consideration for existing data in a live database, hadn’t thought of that, and its good to know how to convert dynamically – also the point about transactions (postgres over mysql).

As for a payment_type_id, I believe its standard to have a numeric id. The short-hand version could be handy for comparison statements when you actually go to implement the payment processing. Which do you want to write?:


if ptype.label == 'Personal Check'
if ptype.name  == 'check'
if ptype.id    == 7 #=> uh...which one is 7 again?

Your label may very well change due to marketing purposes, but the id and the name attributes would have a tendency to remain constant.

Bill says: I also added the DB constraint “knitting” to order.rb:


 belongs_to :payment_type

and in payment_type.rb


class PaymentType < ActiveRecord::Base
  has_one :order
end

sarah says:

After trial and error, I agree with linoj who said “I dont understand the need for the added complexity of a foreign key here”. I originally got this working with with a numeric fk (id), where I was adding payment_type_id and deleting pay_type from orders in my migrations. It was working, but it was fragile on the front-end because pay_type was no longer a method, payment_type_id no longer existed, etc. So I re-did it by creating table pay_types with typeid (pk) and name [typeid is the string “cc” or “check”, etc]. But still when I rolled back migrations so there was no longer a pay_types table, this code in checkout.rhtml failed:


# This FAILED on rollback
<%= form.select :pay_type, 
                PayType.find(:all).map {|p| [p.name, p.typeid]},
            :prompt => "Select a payment method" %>

And I’d have to manually edit checkout.rhtml and put back the original:


# This needed to be restored
<%=form.select :pay_type,
                Order::PAYMENT_TYPES,
               :prompt=>"Select a payment method" %>

I’d also have to restore the PAYMENT_TYPES array and comment out the incorrect validates_inclusion_of lines in order.rb.

It was a lot of manual editing for a migration rollback. So I made a method in pay_type.rb that will return pay_types from the DB, or, if the DB doesn’t exist return pay_types from a pre-existing array (the same one we had hard-coded originally in order.rb). Is this overkill? – after all, how many times, realistically, is one going to be adding and deleting the database table pay_types, and do we really need to encapsulate this? BUT doing this allowed me to drop and add the pay_types table and see no change in the code functionality and no errors. That was enough of a motivation. So:

In pay_type.rb:


def self.get_pay_types
    if PayType.table_exists?
       pts = PayType.find(:all)
       payment_types = []
       pts.each do |pt|
         payment_types << [pt.name,pt.typeid]
       end
    else
       payment_types = [
        # Displayed         #stored in orders table in db
        ["Check",        "check"],
        ["Credit Card",    "cc"],
        ["Purchase Order", "po"]
        ]
    end
    payment_types
  end
Then I changed my checkout.rhtml to:

<%=form.select :pay_type,
            PayType.get_pay_types,
        :prompt=>"Select a payment method" %>

And my order.rb validation to:


validates_inclusion_of :pay_type, :in=>PayType.get_pay_types.map{|disp,value| value}

And now, no matter where I get my pay_types from (db or array), the code works! Does anyone see a reason to not do it this way (or a variant of this way)?

Russell Healy says:

NicolasConnault – regarding migrating existing data – it’s also important when you’re developing as a team, because other developers may have existing test data in their test databases.

Anyway, here’s my migration which does pretty much the same as JimJamesAZ’s, but with way more lines of code!

007_create_payment_types.rb:

class CreatePaymentTypes < ActiveRecord::Migration

  def self.up
    create_table :payment_types do |t|
      t.column :label, :string, :null => false
    end

    add_column :orders, :payment_type_id, :integer

    check = PaymentType.create(:label => "Check")
    credit_card = PaymentType.create(:label => "Credit card")
    purchase_order = PaymentType.create(:label => "Purchase order")

    Order.find(:all).each do |order|
      case order.pay_type
        when "check" 
          order.payment_type = check
        when "cc" 
          order.payment_type = credit_card
        when "po" 
          order.payment_type = purchase_order
      end
      order.save!
    end

    change_column :orders, :payment_type_id, :integer, :null => false
    remove_column :orders, :pay_type
    execute %Q[alter table orders
                add constraint fk_order_payment_type
                 foreign key (payment_type_id)
                 references payment_types(id)]
  end

  def self.down
    add_column :orders, :pay_type, :string, :limit => 10

    check = PaymentType.find(:first, :conditions => [ "label = ?", "check"])
    credit_card = PaymentType.find(:first, :conditions => [ "label = ?", "cc"])
    purchase_order = PaymentType.find(:first, :conditions => [ "label = ?", "po"])

    Order.find(:all).each do |order|
      case order.payment_type
        when check
          order.pay_type = "check" 
        when credit_card
          order.pay_type = "cc" 
        when purchase_order
          order.pay_type = "po" 
      end
      order.save!
    end

    execute %Q[alter table orders drop foreign key fk_order_payment_type]
    remove_column :orders, :payment_type_id
    drop_table :payment_types
  end
end

kimsk112 says:

To make sure that the script by Russell Healy will work, you will need to add a line of code to order.rb. order.rb:

 belongs_to :payment_type

foudfou says:

good to know: my form.select would not initialize correctly, because one column of my table payment_types was called display, which conflicts with a well-known method.


<%= form.select :payment_type_id,
                 PaymentType.find(:all).collect { |p| [ p.display, p.id ] },
                 :prompt => "Sélectionner un type de réglement" %>

i.e.: p.display would refer to the display method of p, instead of the column named “display” of the table payment_types.

solution was to rename the column “name”.

nulbyte says:

A chink in an otherwise good framework is this nonsense over primary keys having to be integers. There has never been a popular consensus in db that said that primary key had to be like that (database ‘agnostic’ all the same). The OP’s solution that includes both the alphanumeric code and the id. This leaves a sour taste in the mouth. Personally I use the a unique index of a the of the :type_id and set :id=>nil (boo!). I can do the foreign key alright. Foreign keys are generally a good idea in terms of integrity, and to avoid late night debugging. Database ‘agnosticism’ shouldn’t mean database ignorance.

James West says:

The problem here seems to be a lot more about the headache of migrations rather than the actual changes needed to get the change in place and working.

There are some excellent ideas here on the steps required to implement the actual interface requirements which I will certainly have a go at but the main problem appears to be caused by the fact that existing data wil lconflict with the payment types table and there seems to be a lot of confuion over how to deal with this in a migration.

In a commercial environment using other development tools I have tackled this problem many times and the solution has been very simple.

The steps to the solution as I see it are as follows 1) Create and populate the payment type table. 2) Add a foreign key to the order table (this must allow blanks and have no referential integrity at this stage.) 3) Update existing records in the order table to set the foreign key to point to the payment type table primary key (SQL is VERY good at this sort of thing.) 4) Set the foreign key on the the order table to have referential integrity and to not allow blanks (This can be done now as all data will be populated. 5) Remove the original payment type field from the order table.

What I would have done in other development environments would be to write a stored procedure to do the work possibly with an update program depending on the complexity of the situation but can all this be done in a migration. I don’t see why not as SQL can be rn as part of a migration.

The same principle applies time and time again in the real world and the above is very definitely the way to approach the solution in my mind there are no issues with key types being numeric that seem to have confused people and is totally database independant.

The only thing that you need to be careful of with this approach is that when you create and populate the payment types table is that you can easily identify the records to update in the order table. so set the value of the Payment Type Name field to one of the following for each of the 3 payment type records you create “check”, “cc”, “po” this will allow an update statement to find all recrods where name = “po” and update them with the key from the correspondingly matched record in the payment type table

You can then go and change the value of the payment type name field in the payment type table to anything you want after the update has been run.

Now all you need to do is apply the changes to the model etc…

Have I done this yet? No! But I’m about to and if I have time I’ll post back here the actual changes I made to get it to work.

JoeyA says:

I’ve updated the checkout view with the following line to display the drop-down based on the new database table:

<%= form.collection_select :payment_type_id, PaymentType.find(:all), :id, :label, options={:prompt => "Select a payment method"} %>
Does anyone know why the prompt doesn’t appear? I.e., the check, credit card, and purchase order options appear but no “Select a payment method.”

PEZ says:

My migration looks much like linoj’s. Then I have this model:

class PaymentType < ActiveRecord::Base
  ALL_OPTIONS = self.find(:all).collect {|pt| [pt.disp, pt.value]}
  ALL_VALUES = ALL_OPTIONS.map {|disp, value| value}
end
In the Order model:

  validates_inclusion_of :pay_type, :in => PaymentType::ALL_VALUES
And the form select:

      <%= form.select :pay_type, PaymentType::ALL_OPTIONS, :prompt => "Select a payment method" %> 

I.e. it’s much like the original code, just moving some of the responsibilities to the PaymentType model. And since most of the wiring is left unchanged the validation still works.

PEZ adds But now when I’ve reached the section about integration testing I stumble across some problems. (I’m reading the 3rd edition beta, if that matters.) I think it has to do with that the test database doesn’t have the payment types. Adding them with a fixture doesn’t help since I’m initializing the constants before the table is populated (that’s what I think happens, I could be wrong). The fixture was just something I tried to see if it would work. It’s not a solution that appeals to me. Anyone who can suggest a strategy for keeping the test database’s lookup tables populated?

McWild says:

After reading all thread, I still can not understand why all of you want to create a new field(payment_type_id) in orders table? Why not use the old(pay_type) field? And why we should manage migration of existed data with zero records in database(I didn’t count that one test record we have)?

Here is my solution of this task:


class Order < ActiveRecord::Base

  has_many :line_items
  has_one :payment_types

  def add_item(item_information)
    line_items << LineItem.new(item_information)
  end

  if PaymentType.table_exists?
    PAYMENT_TYPES = PaymentType.find(:all).collect {|type| [type.label, type.value]}
  else
    PAYMENT_TYPES = [
      # Displayed        Stored in orders table in db
      ["Check",          "check"],
      ["Credit Card",    "cc"],
      ["Purchase Order", "po"]
    ]
  end

  validates_presence_of :name, :address, :email, :pay_type
  validates_inclusion_of :pay_type, :in =>
    PAYMENT_TYPES.map {|disp, value| value}
end

class PaymentType < ActiveRecord::Base
   belongs_to :orders
end

class CreatePaymentTypes < ActiveRecord::Migration
  def self.up
    create_table :payment_types, :options => 'ENGINE=InnoDB DEFAULT CHARSET=utf8' do |t|
      t.string :label, :null => false
      t.string :value, :limit => 10, :null => false
      t.timestamps
    end
  end

  def self.down
    drop_table :payment_types
  end
end

class AddPaymentTypesData < ActiveRecord::Migration
  def self.up
    PaymentType.create(:label => 'Check' ,          :value => "check")
    PaymentType.create(:label => 'Credit Card' ,    :value => "cc")
    PaymentType.create(:label => 'Purchase Order' , :value => "po")
    remove_column :orders, :pay_type
    add_column :orders, :pay_type, :string, :limit => 10, :null => false
    execute "ALTER TABLE orders ADD CONSTRAINT fk_orders_payment_types FOREIGN KEY (pay_type) REFERENCES payment_types(value)" 
  end

  def self.down
    execute "ALTER TABLE orders DROP FOREIGN KEY fk_orders_payment_types" 
    remove_column :orders, :pay_type
    add_column :orders, :pay_type, :string, :limit => 10, :default => 'check'
  end
end

DougEmery says:

I wanted to use the association of PayType to Order, and be very deliberate in my steps , so I:

  1. created a PayType scaffold (which, among other things, created a migration for the pay_types table),
  2. added two more migrations for altering the order table and adding the pay_type values,
  3. added the has_many and belongs_to associations to PayType and Order, resp.,
  4. changed Order’s validates_inclusion_of to validates_associated
  5. changed checkout.html.erb file to use PayType.find

Here are the details:

1) Create PayType scaffold:


[depot]>ruby script/generate scaffold pay_type name:string

The code of db/migrate/20081211161906_create_pay_types.rb:


class CreatePayTypes < ActiveRecord::Migration
  def self.up
    create_table :pay_types do |t|
      t.string :name # could set this to :null => false

      t.timestamps
    end
  end

  def self.down
    drop_table :pay_types
  end
end

2a) Add migration for changes to order table:


[depot]>ruby script/generate migration add_pay_type_id_to_order \
> pay_type_id:integer

Edit migration db/migrate/20081211162213_add_pay_type_id_to_order.rb:


class AddPayTypeIdToOrder < ActiveRecord::Migration
  def self.up
>    remove_column :orders, :pay_type
    add_column :orders, :pay_type_id, :integer
  end

  def self.down
    remove_column :orders, :pay_type_id
  end
end

2b) Add migration for adding pay_type values:


[depot]>ruby script/generate migration add_pay_types

Edit migration db/migrate/20081211162456_add_pay_types.rb


class AddPayTypes < ActiveRecord::Migration
  def self.up
>    PayType.delete_all
>    PayType.create(:name => 'Check')
>    PayType.create(:name => 'Credit Card')
>    PayType.create(:name => 'Purchase Order')
  end

  def self.down
>    PayType.delete_all
  end
end

3) Added the has_many and belongs_to associations to PayType and Order, resp.,


class PayType < ActiveRecord::Base
>  has_many :orders
end

class Order < ActiveRecord::Base
>  # PAYMENT_TYPES = [
>  #   # Displayed         stored in db
>  #   [ "Check",          "check"],
>  #   [ "Credit card",    "cc"],
>  #   [ "Purchase order", "po"]
>  # ]

>  belongs_to :pay_type
  has_many :line_items

 #...
end

4) Change Order’s validates_inclusion_of to validates_associated:


class Order < ActiveRecord::Base

  belongs_to :pay_type
  has_many :line_items

  validates_associated :pay_type
  # validates_inclusion_of :pay_type, 
  #             :in => PAYMENT_TYPES.map { |disp,value| value }

  #...
end

5) Change checkout.html.erb file to use PayType.find:


        <p>
            <%= form.label :pay_type, "Pay with:" %>
            <%= 
                form.select :pay_type,
>                           PayType.find(:all).collect { |pt| [ pt.name, pt.id ] },
                            :prompt => "Select a payment method" 
             %>
        </p>

This did not work as expected, though, because the select returned an ‘id’ and the Order object pay_type field expected a PayType object, so I got:


ActiveRecord::AssociationTypeMismatch in StoreController#save_order

PayType(#19767680) expected, got String(#111600)

The solution was to change :pay_type to the implicit :pay_type_id value that is created by the belongs_to :pay_type association. I found the answer here:

http://wiki.rubyonrails.org/rails/pages/HowtoUseFormOptionHelpers


        <p>
            <%= form.label :pay_type, "Pay with:" %>
            <%= 
                form.select :pay_type_id,
                            PayType.find(:all).collect { |pt| 
                                             [ pt.name, pt.id ] },
                            :prompt => "Select a payment method" 
             %>
        </p>

Trientalis said:

I found a solution similar to the one of PEZ, but a little different:

My first migration is:


class CreatePayments < ActiveRecord::Migration
  def self.up
    create_table :payments do |t|
      t.string :name
      t.string :short

      t.timestamps
    end
  end

  def self.down
    drop_table :payments
  end
end

... and my second migration populates the payment table with data:


class AddPaymentMethods < ActiveRecord::Migration
  def self.up
    Payment.create(:name => "Check", :short => "check")
    Payment.create(:name => "Credit card", :short => "cc")
    Payment.create(:name => "Purchase order", :short => "po")
  end

  def self.down
    Payment.delete_all
  end
end

Sure, you can do this as one migration, but to roll the migrations back it might be better to seperate these two steps.

Then I did the following in the new Payment model to make the data useable to other controllers:


class Payment < ActiveRecord::Base

  ALL = self.find(:all, :order=>:name).map do |s|
    [s.name, s.short]
  end

end

I named the displayed string “name” and the value “short”. Now by “ALL” I can call these data in the Order model. And I changed the validation a little so that it works with my column names


class Order < ActiveRecord::Base

  has_many :line_items

  PAYMENT_TYPES = Payment::ALL

  validates_presence_of :name, :address, :email, :pay_type 
  validates_inclusion_of :pay_type, :in => 
  PAYMENT_TYPES.map {|name, short| short}
. 
.
.
end 

... and in the checkout file I replaced the old form.select by this:


        <%= 
        form.select :pay_type, 
        Payment::ALL, 
        :prompt => "Select a payment method!" 
        %>    

Because I have made the crucial change in the Payment model, the original code still works without many further changes in the order table or so being necessary.

Will says

Move comments for the book to StackOverflow! Would be much easier to read and vote up the best answer IMHO