第10章 付账
就目前而言,我们只须获取买主的详细联系信息和付款方式,并用这些信息在数据库里构造一份订单即可。
收集订单信息
订单(order)是由一组订单项(line item)、外加购买交易的详细信息构成的。
ruby script/generate scaffold order name:string address:text email:string pay_type:string
ruby script/generate scaffold line_item product_id:integer order_id:integer quantity:integer total_price:decimal
depot_p/db/migrate/20080601000005_create_orders.rb
→ t.string :pay_type, :limit => 10
depot_p/db/migrate/20080601000006_create_line_items.rb
→ t.integer :product_id, :null => false, :options => "CONSTRAINT fk_line_item_products REFERENCES products(id)"
→ t.integer :order_id, :null => false, :options => "CONSTRAINT fk_line_item_orders REFERENCES orders(id)"
→ t.integer :quantity, :null => false
→ t.decimal :total_price, :null => false, :precision => 8, :scale => 2
这张表包含两个外键。这是因为line_items 表中的每条记录都需要同时与“订单”和“货品”关联。 这里有几个问题,首先, Rails 迁移任务并未提供一种独立于数据库的方式指定外键约束,其次,不会真正执行外键约束;最后,这些自定义的约束不会被储存在db/schema.rb 文件中,因此也不会被复制到测试数据库。
rake db:migrate
模型之间的关系
数据库已经知道订单、订单项与货品之间的关系了,但Rails 应用还不知道。所以,我们还需要在模型类中增加一些声明,指定它们之间的关系。(Rails为什么要知道?知道了之后可以做什么事情?)
注意:需要指明关系的模型是那些需要保存在数据库中的,像cart_item就不必指明belongs_to :cart这种关系
class Order < ActiveRecord::Base
has_many :line_items
end
class Product < ActiveRecord::Base
has_many :line_items
#......
end
class LineItem < ActiveRecord::Base
belongs_to :order
belongs_to :product
end
这些声明到底管什么用?简单说来,它们会给模型对象加上彼此导航的能力。由于在LineItem模型中添加了belongs_to 声明,现在我们就可以直接得到与之对应的Order 对象了。 (每一种关系都会增加相应的attr或者method)
li = LineItem.find(...)
puts "This line item was bought by #{li.order.name}"
order = Order.find(...)
puts "This order has #{order.line_items.size} line items"
创建表单搜集订单信息
现在数据库表和模型类都已经到位,我们可以开始处理付账的流程了。首先,我们需要在购物车里加上一个Checkout 按钮,并使其连接到store 控制器的checkout 方法。
depot_p/app/views/store/_cart.html.erb
<%= button_to "Checkout" , :action => 'checkout' %>
depot_p/app/controllers/store_controller.rb
def checkout
@cart = find_cart
if @cart.items.empty ?
redirect_to_index("Your cart is empty" )
else
@order = Order.new
end
end
随后,当用户点击“提交”按钮时,我们希望在控制器中将来自表单的新数据取回到模型对象中。
幸运的是, Rails 使这一任务变得易如反掌:它提供了一组用于处理表单的辅助方法(就是helper模块?),这些辅助方法会与控制器和模型对象交互,实现了完整的表单处理功能。
例子
<% form_for :order, :url => { :action => :save_order } do |form| %>
<p>
<label for="order_name" >Name:</label>
<%= form.text_field :name, :size => 40 %>
</p>
<% end %>
传入的第一个参数:order 告诉该方法:它正在处理的是来自order实例变量的对象。辅助方法会根据这一信息给字段命名,并安排如何将字段的输入值传回给控制器。 由于这个文本字段是在form_for 的上下文环境中构造出来的,因此它就自动地与@order 对象中的数据建立起关联了。
Rails 需要知道每个字段的名称和值,这样才能将其与模型对象关联;而form_for 和各种字段层面的辅助方法( 例如text_field)正是用于提供这些信息的。
depot_p/app/views/store/checkout.html.erb
<div class="depot-form">
<%= error_messages_for 'order' %>
<% form_for :order, :url => { :action => :save_order } do |form| %>
<fieldset>
<legend>Please Enter Your Details</legend>
<div>
<%= form.label :name, "Name:" %>
<%= form.text_field :name, :size => 40 %>
</div>
<div>
<%= form.label :address, "Address:" %>
<%= form.text_area :address, :rows => 3, :cols => 40 %>
</div>
<div>
<%= form.label :email, "E-Mail:" %>
<%= form.text_field :email, :size => 40 %>
</div>
<div>
<%= form.label :pay_type, "Pay with:" %>
<%=
form.select :pay_type,
Order::PAYMENT_TYPES,
:prompt => "Select a payment method"
%>
</div>
<%= submit_tag "Place Order" , :class => "submit" %>
</fieldset>
<% end %>
</div>
depot_p/app/models/order.rb
PAYMENT_TYPES = [
# Displayed stored in db
[ "Check" , "check" ],
[ "Credit card" , "cc" ],
[ "Purchase order" , "po" ]
]
第一个元素是一个字符串,代表将在下拉列表框中显示的文本;第二个元素是将被提交并最终存入数据库的值
depot_p/app/models/order.rb
class Order < ActiveRecord::Base
validates_presence_of :name, :address, :email, :pay_type
validates_inclusion_of :pay_type, :in => PAYMENT_TYPES.map {|disp, value| value}
# ...
搜集订单详细信息
depot_p/app/controllers/store_controller.rb
def save_order
@cart = find_cart
@order = Order.new(params[:order])
@order.add_line_items_from_cart(@cart)
if @order.save
session[:cart] = nil
redirect_to_index("Thank you for your order" )
else
render :action => :checkout
end
end
我们新建了一个Order 对象,并用表单数据对其进行初始化。在这里,我们希望所有的表单数据都与Order 对象关联,所以我们直接从参数中取出:order 这个hash( 这也正是我们传递给form_for 的第一个参数的名字) ,用它来构造Order 对象。
depot_p/app/models/order.rb
def add_line_items_from_cart(cart)
cart.items.each do |item|
li = LineItem.from_cart_item(item)
line_items << li
end
end
depot_p/app/models/line_item.rb
def self.from_cart_item(cart_item)
li = self.new
li.product = cart_item.product
li.quantity = cart_item.quantity
li.total_price = cart_item.price
li
end
Ajax 修改
在接受订单之后,我们将用户重定向回到首页,并显示一条flash 信息:“ Thank you for your order .”这条flash 信息会一直显示在页面上。要在选购物品之后把包含flash 信息的<div> 隐藏起来即可。
首先想到的办法是在add_to_cart.js.rjs 中加上
page[:notice].hide
但是当我们首次进入在线商店时, flash 中什么都没有,所以id 叫notice 的这个<div> 也不会显示出来,最终的解决方案有些类似于hack :只有当这个<div> 存在时,我们才想运行.hide 方法。但RJS 并没有提供这种“检查div 是否存在”的功能。不过,它允许我们遍历页面上所有与指定的CSS selector 模式匹配的元素,所以我们不妨遍历所有id 为notice 的<div> 标记。
depot_p/app/views/store/add_to_cart.js.rjs
→ page.select('div#notice' ).each { |div| div.hide }