说明:
开发环境配置:
OS:winxp
1.安装ruby版本:1.8.6  下载地址[url]ftp://ftp.ruby-lang.org/pub/ruby/1.8/ruby-1.8.6.zip[/url]
2.安装rails 版本:1.2.5  gem install rails -y
3.安装数据库:mysql- 5.0.22-win32     gem install mysql
--下载地址:
--http://download.mysql.cn/downloa ... ql-5.0.22-win32.zip
--修改一下安装路径C:\MySQL,剩下的安装默认安装即可
--配置环境变量,添加C:\MySQL\bin;

Case1.helloworld!
##这里必须要记住的是这三个CMD命令
1>rails demo     #create一个rails应用
2.demo>ruby script/server      #启动这个rails应用
3.demo>ruby script/generate controller Say    #新建一个控制器
在控制器的脚本中写一个action,(hello),理解一下controller和view的关系,controller invokes view(控制器调用视图)
然后在view里面就生成了一个hello.rhtml。代码:
1  <html>
2     <head>
3      <title>hello, rails!</title>
4     </head>
5     <body>
6      <h1>Hello from Rails!</h1>
7     </body>
8  </html>

链接:
再创建goodbye的action,类似的编写goodbye.rhml代码
在hello.rhtml里加入下面的Ruby代码:
<p>Time to say<%=link_to "Goodbye!", :action =>"goodbye" %></p>
:action是一个ruby的symbol,看做”名叫某某某的东西”,因此:action就代表”名叫action”的东西(名片)
<%= ...%>写ruby的地方。
这样就链接起两个页面
 
动态模板的创建
1.    构建器Builder【以后讲】
2.    将Ruby代码嵌入到模板中。
.rhtml后缀告诉Rails,需要借助ERb系统对文件内容进行扩展---ERb就是用于将Ruby代码嵌入模板文件中。
普通内容进入rhtml直进直出,没有变化,<%=…%>符号之间的内容是Ruby代码,执行结果被转换成字符串,并替换<%...%>所在位置的内容。譬如在hello.rhtml中加入如下代码
1  <html>
2     <head>
3      <title>hello, rails!</title>
4     </head>
5     <body>
6      <h1>Hello from Rails!</h1>
7      <ul>            #ul绘制文本的项目符号列表。
8       <li>Addition: <%=1+2%></li>                            #li引起列表中的一个项目。
9       <li>Concatenation:<%="cow" + "boy"%> </li>
10       <li>Time in one hour :<%= 1.hour.from_now%></li>
11      </ul>
12     </body>
13  </html>
 
# 如果是<%...%>就是Ruby代码的执行,如果加上等号最后就转换成了一段string。
1   <%3.times do%>
2        ho!<br/>    #<br/>插入一个换行符。
3   <%end%>
Html源文件显示:
 
在<%...%>改变成<%...-%>
1  <%3.times do-%>
2        ho!<br/>   
3   <%-end%>
Html源文件显示
 
但是在浏览器里不会有变化,一直都是这样显示
 
动态内容放入HTML页面中时应该使用h()方法
例如:Email :<%= h("Ann & Bill <[email]frazers@isp.emai[/email]l>")%>
h()方法保护了邮件地址中的特殊字符不会扰乱浏览器显示
3.    rjs 为现有页面添加Ajax魔法【以后讲】
学习总结:
    如何新建一个Rails应用程序,以及如何在其中新建控制器
    Rails是如何将用户请求映射到程序代码的
    如何在控制器中创建动态内容,以及如何在视图模板中显示动态内容
    如何把各个页面链接起来
depot项目迭×××发
增量式开发(Incremental Development),快速迭代思想,这个地方首先阅读一些敏捷开发和快速迭代的资料。
任务A:
    迭代A1.跑起来再说
    迭代A2.添加缺失字段
    迭代A3.检查一下(逻辑验证)
    迭代A4.更完美的列表页

迭代1:跑起来再说
    用脚手架,跑起来一个卖家的应用,可以添加数据…

>rails depot      #创建rails应用depot

depot>mysqladmin -u root create depot_development     #创建数据库

depot>ruby script/generate model product     #创建product模型类
 
创建模型类的同时会创建这两个文件:
迁移任务 : 001_create_products.rb   
模型类  :product.rb
  #说明:迁移任务的名字:描述自己的功能(create_products[自动加了复数]),序列号 (001),扩展名(.rb)。
  打开db/migrate目录下的001_create_products.rb,添加代码来创建数据库表(products表的内容)
1  class CreateProducts < ActiveRecord::Migration
2  def self.up                         # up()方法用于实施迁移
3   create_table :products do |t|
4     # t.column :name, :string           #定义数据库表的代码     

6        t.column :title,           :string
7        t.column :description,     :text
8        t.column :p_w_picpath_url,       :string
9    end
10  end
11 
12  def self.down     # down()方法负责撤销up()方法的效果当需要把数据库恢复到前一个版本时13  就会执行这个方法
14      drop_table :products
15   end
16  end

depot>rake db:migrate     #数据迁移

products表会被添加到database.yml文件的development
schema_info这张表的作用就是跟踪数据库的版本。如果要撤销数据迁移,可以用这句:rake db:migrate version=0这里0就恢复到了没有做过数据迁移的情况,1,2,3,4…根据实际情况使用。
depot>ruby script/generate controller admin      #创建卖家控制器admin
------------------------------------
在admin这个控制器中加上脚手架(scaffold)
-------------------------------------
1  class AdminController < ApplicationController
2     scaffold :product    #scaffold 运行时生成应用程序代码 :product模型来维护数据。
3  end
----------------------------------------
depot>ruby script/server     #启动rails应用
[url]http://localhost:3000/admin[/url]
迭代2.添加缺失的字段
    任务: 在products表中添加缺失的字段price,price是浮点类型,8位有效数字,保留2位有效数字,默认值是0.0
depot>ruby script/generate migration add_price     #新建迁移任务,添加price字段
002_add_price.rb
1  class AddPrice < ActiveRecord::Migration

3    def self.up

5      add_column :products, :price, :decimal, :precision =>8, :scale=>2, :default=>0
6  #                 ↑      ↑       ↑     ↑            ↑
7  #               表的名字  字段名  类型 有效数字     保留两位有效数字
8    end

10    def self.down
11    remove_column :products, :price
12    end
13  end
 
column(name, type, options = {})
Instantiates a new column for the table. The type parameter must be one of the following values: :primary_key, :string, :text, :integer, :float, :decimal, :datetime, :timestamp, :time, :date, :binary, :boolean.
Available options are (none of these exists by default):
•    :limit: Requests a maximum column length (:string, :text, :binary or :integer columns only)
•    :default: The column‘s default value. Use nil for NULL.
•    :null: Allows or disallows NULL values in the column. This option could have been named :null_allowed.
•    :precision: Specifies the precision for a :decimal column.
•    :scale: Specifies the scale for a :decimal column.
Please be aware of different RDBMS implementations behavior with :decimal columns:
•    The SQL standard says the default scale should be 0, :scale <= :precision, and makes no comments about the requirements of :precision.
•    MySQL: :precision [1..63], :scale [0..30]. Default is (10,0).
•    PostgreSQL: :precision [1..infinity], :scale [0..infinity]. No default.
•    SQLite2: Any :precision and :scale may be used. Internal storage as strings. No default.
•    SQLite3: No restrictions on :precision and :scale, but the maximum supported :precision is 16. No default.
•    Oracle: :precision [1..38], :scale [-84..127]. Default is (38,0).
•    DB2: :precision [1..63], :scale [0..62]. Default unknown.
•    Firebird: :precision [1..18], :scale [0..18]. Default (9,0). Internal types NUMERIC and DECIMAL have different storage rules, decimal being better.
•    FrontBase?: :precision [1..38], :scale [0..38]. Default (38,0). WARNING Max :precision/:scale for NUMERIC is 19, and DECIMAL is 38.
•    SqlServer?: :precision [1..38], :scale [0..38]. Default (38,0).
•    Sybase: :precision [1..38], :scale [0..38]. Default (38,0).
•    OpenBase?: Documentation unclear. Claims storage in double.
This method returns self.
Examples
 # Assuming td is an instance of TableDefinition
 td.column(:granted, :boolean)
   #=> granted BOOLEAN

 td.column(:picture, :binary, :limit => 2.megabytes)
   #=> picture BLOB(2097152)

 td.column(:sales_stage, :string, :limit => 20, :default => 'new', :null => false)
   #=> sales_stage VARCHAR(20) DEFAULT 'new' NOT NULL

 def.column(:bill_gates_money, :decimal, :precision => 15, :scale => 2)
   #=> bill_gates_money DECIMAL(15,2)

 def.column(:sensor_reading, :decimal, :precision => 30, :scale => 20)
   #=> sensor_reading DECIMAL(30,20)

 # While <tt>:scale</tt> defaults to zero on most databases, it
 # probably wouldn't hurt to include it.
 def.column(:huge_integer, :decimal, :precision => 30)
   #=> huge_integer DECIMAL(30)

 
depot>rake db:migrate     #迁移数据

depot>ruby script/server            #启动depot
---------------
迭代A3,检查一下(加入验证逻辑)
任务:给输入数据加上验证
1.    非空字段:title, description, img_url;
2.    必须是数字的字段: price
3.    price字段的值大于0
4.    title的唯一性
5.    img_url的格式验证:必须是png, jpg, bmp,gif结尾的字符串

models/product.rb
---------
1  class Product < ActiveRecord::Base

3    validates_presence_of :title, :description, :p_w_picpath_URL
4    validates_numericality_of  :price     #验证price是否是数字
5    validates_uniqueness_of :title        #验证title字段在表中是否唯一
6    validates_format_of  :p_w_picpath_url,          #验证图片格式是否正确
7                              :with  =>  /\.(gif|jpg|png)$/i,
8                              :message =>"must be a URL for a GIF, JPG or PNG p_w_picpath!"
9  protected                    #protected,是因为该方法必须在特定的模型上下文中调用,不能在外部随便调用
10  def validate           #validate()的方法,在保存product实例前自动调用这个方法用它来检查字段合法性
11    errors.add(:price, "should be at least 0.01") if price.nil? || price<0.01
12  #先应该检查一下它是不是nil。如果是nil与0.01进行比较,就会发生一个异常。
13  end
14  end
----------------------------------------------------------------------------------------------------------------------------------------------
 
Protected Instance methods
 validate( )
Overwrite this method for validation checks on all saves and use errors.add(field, msg) for invalid attributes.
迭代A4:更美观的列表页
任务 1. 把动态脚手架变成静态脚手架
2.编辑静态脚手架生成的代码
3.利用数据迁移加入测试数据.
4.加入css
depot>ruby script/generate scaffold product admin                     #生成静态的脚手架

overwrite app/controllers/admin_controller.rb? [Ynaqd] y
       force  app/controllers/admin_controller.rb
overwrite test/functional/admin_controller_test.rb? [Ynaqd] y
       force  test/functional/admin_controller_test.rb
用IDE(NetBeans)时,注意要写上model name ,controller name. When File Alreay Exist 在overwrite上选择
App/views/admin/list.rhtml文件中rails视图生成了当前的货品列表页面。
测试数据:
1.用可控的方式来填充数据,使用迁移任务。
depot>ruby script/generate migration add_test_data
修改003_add_test_data.rb:(/depot_c/db/migrate/003_add_test_data.rb)
顺便把图片和depot.css也拷贝到public\p_w_picpath和public\stylesheets目录下!
depot>rake db:migrate              #执行数据迁移
引用CSS样式!(要看到css样式,需要重新启动应用):
views\layouts\admin.rhtml
  <%= stylesheet_link_tag 'scaffold' ,'depot' %>         #添加depot.css
接下来就是修改/app/views/admin/list.rhtml了!
拷贝一下
现在在浏览器中输入[url]http://localhost:3000/admin/list[/url]就可以看到

总结:
    创建了开发数据库,并对Rails应用进行配置,使之能够访问开发数据库
    用迁移任务创建和修改了开发数据库的结构,并向其中装载了测试数据
    创建了Products表,并用脚手架生成器编写了一个应用程序,用于维护表中保存的货品信息
    在自动生成的代码基础上增加了对输入的验证
    重写了自动生成的视图代码,使之更美观
任务B:分类显示(将货品列表界面加以美化)
    迭代B1.创建分类列表
    迭代B2.添加页面布局
    迭代B3.用辅助方法格式化价格
    迭代B4.链接到购物车
迭代B1.创建分类列表
前面创建了Admin控制器,卖主用它来管理Depot应用
现在,创建第二个控制器Store,与付钱的买主交互的控制器.
generate controller store index    #创建控制器store和一个action(index)
store_controller.rb
1  def index
2      @products=Product.find_products_for_sale
3   end
这里调用Product.find_products_for_sale, 调用了Product的一个类方法,所以在Model里知道product.rb进行定义此方法。
问:models里的文件都是对应的类,为什么创建model类的同时会自动创建数据库呢?
答:以后单独创建类文件的时候可以不用generate,直接新建一个类文件到model里!
models/product.rb

1  def self.find_products_for_sale
2      find(:all, :order=>"title")
3  end
#这里使用了Rails提供的find()方法,:all参数代表我们希望取出符合指定条件的所有记录。通过询问用户,我们确定了关于“如何对列表进行排序”,先按照货品名称排序,看看效果。于是:order=>”title”
find(*args)
Find operates with three different retrieval approaches:
•    Find by id: This can either be a specific id (1), a list of ids (1, 5, 6), or an array of ids ([5, 6, 10]). If no record can be found for all of the listed ids, then RecordNotFound will be raised.
•    Find first: This will return the first record matched by the options used. These options can either be specific conditions or merely an order. If no record can matched, nil is returned.
•    Find all: This will return all the records matched by the options used. If no records are found, an empty array is returned.
All approaches accept an option hash as their last parameter. The options are:
•    :conditions: An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro.
•    :order: An SQL fragment like "created_at DESC, name".
•    :group: An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause.
•    :limit: An integer determining the limit on the number of rows that should be returned.
•    :offset: An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows.
•    :joins: An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id". (Rarely needed). The records will be returned read-only since they will have attributes that do not correspond to the table‘s columns. Pass :readonly => false to override.
•    :include: Names associations that should be loaded alongside using LEFT OUTER JOINs. The symbols named refer to already defined associations. See eager loading under Associations.
•    :select: By default, this is * as in SELECT * FROM, but can be changed if you for example want to do a join, but not include the joined columns.
•    :from: By default, this is the table name of the class, but can be changed to an alternate table name (or even the name of a database view).
•    :readonly: Mark the returned records read-only so they cannot be saved or updated.
•    :lock: An SQL fragment like "FOR UPDATE" or "LOCK IN SHARE MODE". :lock => true gives connection‘s default exclusive lock, usually "FOR UPDATE".
Examples for find by id:
  Person.find(1)       # returns the object for ID = 1
  Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6)
  Person.find([7, 17]) # returns an array for objects with IDs in (7, 17)
  Person.find([1])     # returns an array for objects the object with ID = 1
  Person.find(1, :conditions => "administrator = 1", :order => "created_on DESC")
Examples for find first:
  Person.find(:first) # returns the first object fetched by SELECT * FROM people
  Person.find(:first, :conditions => [ "user_name = ?", user_name])
  Person.find(:first, :order => "created_on DESC", :offset => 5)
Examples for find all:
  Person.find(:all) # returns an array of objects for all the rows fetched by SELECT * FROM people
  Person.find(:all, :conditions => [ "category IN (?)", categories], :limit => 50)
  Person.find(:all, :offset => 10, :limit => 10)
  Person.find(:all, :include => [ :account, :friends ])
  Person.find(:all, :group => "category")
Example for find with a lock. Imagine two concurrent transactions: each will read person.visits == 2, add 1 to it, and save, resulting in two saves of person.visits = 3. By locking the row, the second transaction has to wait until the first is finished; we get the expected person.visits == 4.
  Person.transaction do
    person = Person.find(1, :lock => true)
    person.visits += 1
    person.save!
  end
#find()方法返回一个数组,其中每个元素是一个Product对象,分别代表从数据库返回的一条记录。find_products_for_sale()方法直接把这个数组返回给控制器。
views/store/index.rhtml
1  <h1>Your Pragmatic Catalog</h1>   #标题
2  <%for product in @products -%>    #ruby语句,循环货品
3    <div class='entry'>
4    <img src="<%=product.p_w_picpath_url%>"/>
5   <h3><%=h(product.title)%></h3>
6    <%=product.description%>
7    <span class="price"><%=product.price%></span>
8    </div>
9  <%end%>
 
迭代B2.添加页面布局
任务:
添加一个布局文件
布局(layout)是一个模板,可以将内容填充到模板。可以把所有的在线商店页面定义同样的布局,然后把“分类列表”的页面塞进这个布局里,稍后,我们可以把“购物车”和“结账”的页面也套进同样的布局。因为只有一个布局模板,我们只需要修改一个文件,就可以改变整个站点的观感。
在Rails中,定义和使用布局的方式有好多种,我们现在使用最简单的一种,如果在/app/views/layouts目录中创建一个与某个控制器同名的模板文件,那么该控制器所渲染的视图在默认状态下会使用此模板布局。所以下面就来创建这样一个模板。创建与store同名的模板文件store.rhtml
views/layouts/store.rhtml
1  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
2                        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
3  <html>
4  <head>
5    <title>Pragprog Books Online Store</title>
6    <%= stylesheet_link_tag "depot", :media => "all" %>
7  </head>
8  <body id="store">
9    <div id="banner">
10      <%= p_w_picpath_tag("logo.png") %>
11      <%= @page_title || "Pragmatic Bookshelf" %>
12    </div>
13    <div id="columns">
14      <div id="side">
15        <a href="http://www....">Home</a><br />
16        <a href="http://www..../faq">Questions</a><br />
17        <a href="http://www..../news">News</a><br />
18        <a href="http://www..../contact">Contact</a><br />
19      </div>
20      <div id="main">
21        <%= yield :layout %>
22      </div>
23    </div>
24  </body>
25  </html>
重新启动rails应用
 
迭代B3.用辅助方法格式化价格
    将数据库中以数字形式保存的价格变成“元+分”的形式,例如值为12.34的价格应该显示为$12.34,”13”应该显示为”$13.00”
可以这样进行格式化:
view/index.rhtml
<span class =”price”><%=sprintf(“$%0.2f”, product.price)%></span>
但是,如果我们以后对应用程序进行国际化,会给维护带来麻烦。用一种单独的辅助方法来处理“价格格式化”的逻辑。view/index.rhtml
<span class="price"><%=number_to_currency(product.price)%></span>
number_to_currency(number, options = {})
Formats a number into a currency string. You can customize the format in the options hash.
•    :precision - Sets the level of precision, defaults to 2
•    :unit - Sets the denomination of the currency, defaults to "$"
•    :separator - Sets the separator between the units, defaults to "."
•    :delimiter - Sets the thousands delimiter, defaults to ","
 number_to_currency(1234567890.50)     => $1,234,567,890.50
 number_to_currency(1234567890.506)    => $1,234,567,890.51
 number_to_currency(1234567890.506, :precision => 3)    => $1,234,567,890.506
 number_to_currency(1234567890.50, :unit => "&pound;", :separator => ",", :delimiter => "")
    => &pound;1234567890,50
迭代B4.链接到购物车
    任务:添加一个Add to cart 按钮,可以把货品添加到购物车
index.rhtml中添加语句:
  <%=button_to "Add to Cart", :action=>:add_to_cart, :id=>product%>
--help
button_to(name, options = {}, html_options = {})
Generates a form containing a single button that submits to the URL created by the set of options. This is the safest method to ensure links that cause changes to your data are not triggered by search bots or accelerators. If the HTML button does not work with your layout, you can also consider using the link_to method with the :method modifier as described in the link_to documentation.
The generated FORM element has a class name of button-to to allow styling of the form itself and its children. You can control the form submission and input element behavior using html_options. This method accepts the :method and :confirm modifiers described in the link_to documentation. If no :method modifier is given, it will default to performing a POST operation. You can also disable the button by passing :disabled => true in html_options.
  button_to "New", :action => "new"
Generates the following HTML:
  <form method="post" action="/controller/new" class="button-to">
    <div><input value="New" type="submit" /></div>
  </form>
If you are using RESTful routes, you can pass the :method to change the HTTP verb used to submit the form.
  button_to "Delete Image", { :action => "delete", :id => @p_w_picpath.id },
            :confirm => "Are you sure?", :method => :delete
Which generates the following HTML:
  <form method="post" action="/p_w_picpaths/delete/1" class="button-to">
    <div>
      <input type="hidden" name="_method" value="delete" />
      <input confirm('Are you sure?');"
                value="Delete" type="submit" />
    </div>
  </form>

总结:
    新建一个控制器,用于处理买主的交互
    实现默认的index() action
    在Product模型类中添加一个类方法,使之返回所有可销售的货品项
    实现一个视图(rhtml文件)和一个布局模板(同样是rhtml文件),用于显示货品列表
    创建一个简单的样式表
    使用辅助方法对价格信息进行格式化
    给每件货品增加一个按钮,让用户可以将其放入自己的购物车
任务C:创建购物车
session
    迭代C1:创建购物车
    迭代C2:更聪明的购物车
    迭代C3:处理错误
    迭代C4:结束购物车
把session放在数据库中
Rails可以很容易地把session数据保存在数据库中。用Rake任务创建所需要的数据库表
1.rake db:sessions:create
数据迁移
2.rake db:migrate
3.在config/environment.rb文件中找到
  # config.action_controller.session_store = :active_record_store
把#去掉,就会激活基于数据库的session存储机制
购物车和session
在strore控制器中加入方法find_cart()方法
1  def find_cart
2      unless session[:cart]          #if there’s no cart in the session
3        session[:cart]=Cart.new     #add a new one
4      end
5      session[:cart]          #return existing or new cart
6   end
在控制器中使用当前session就像使用一个hash一样—:cart是一个symbol,作为索引。而且Cart是一个类。
更常用的方式:
1    private
2    def find_cart
3     session[:cart]||=Cart.new     # Ruby的条件赋值操作符 ||=.
4    end
||=的作用:如果session的hash中已经有:cart这个键,上述语句会立即返回:cart键对应的值;否则新建一个Cart对象,并放入session,并返回新建的对象。
#注意,这里声明的private , Rails不会将其作为控制器中的一个action, 此外要小心的---如果把这些方法都放在private声明的后面,从控制器之外就看不到它们,新的action必须位于private之前!
迭代C1:创建购物车
已经创建了session来存储购物车了,购物车是个类,这个类有一些数据,再加上一些业务逻辑,所以从逻辑上来说应该是一个模型对象。这里,不需要一张数据库表与之对应了。因为购物车与买家session紧密相关,而session数据又是所有服务器都可以访问到的(应用程序可能会部署在多服务器的环境下),所以完全不必再额外存储购物车了。因此先用一个普通的类来实现购物车,看看有什么效果,这个类实现:目前它只是包装了一个数组,其中存放一组货品。当顾客往购物车中添加货品(用add_product()方法)时,选中的货品就会被添加到数组之中。
创建一个cart.rb(不是generate),放在model目录下
1  class Cart
2    attr_reader :items           #@items可读写属性打开

4    def initialize                #构造函数
5      @items=[]                 #初始化数组@items
6    end

8    def add_product(product)  #定义加入购物车货品
9      @items<<product       #将货品id加入购物车
10    end
11 
12    def total_price          #定义总价格的方法
13      @items.sum { |item| item.price }      #数组的sum方法
14    end
15  end

  前面在“分类列表”视图已经为每种货品提供了“Add to Cart”链接
<%=button_to "Add to Cart", :action=>:add_to_cart, :id=>product%>
这个链接指向store 控制器的 add_to_cart() action方法(这个方法暂时还不存在),并传入货品的ID作为参数(:id=>product实际上是:id=>product.id的缩写,两者都会将货品的id传回给控制器)
模型中的id字段的重要性:rails根据这个字段来识别模型对象的身份,以及它们对应的数据库表。只要将一个id传递给add_to_cart()方法,我们就可以唯一地标志要加入购物车的货品。

写add_product方法,在store_controller.rb中,写在private上面(action方法)
controllers/store_controller.rb
1  def add_to_cart
2      @cart=find_cart                   #从session中找出购物车对象(或者新建一个)
3      product=Product.find(params[:id])   #利用params对象从请求中取出id参数,随后请求Product模型
4                                                                        #根据id找出货品
5      @cart.add_product(product)         #把找出的货品放入购物车
6  end
params 是Rails应用中一个重要的对象,其中包含了浏览器请求传来的所有参数。按照惯例,params[:id]包含了将被action使用的对象id(或者说主键) 。在视图中调用button_to时我们已经使用:id=>product把这个值设置好了
再写add_to_cart这个action对应的rhtml
代码:
1  <h1>Your Pragmatic Cart</h1>

3  <ul>
4      <%for item in @cart.items%>    #在购物车数组里执行循环体
5        <li><%=h(item.title)%></li>    #显示购物车里或货品的title
6      <%end%>
7   
8  </ul>
运行看看起来
迭代C2.更聪明的购物车
在model下新建一个ruby文件,cart_item.rb,其中引用一种货品,并包含数量信息。(新建一个类就要新建一个model下的ruby文件)
1  class CartItem
2    attr_reader :product, :quantity
 
3    def initialize(product)   #构造函数
4      @product=product
5      @quantity=1
6    end
7   
8    def increment_quantity  
9      @quantity+=1
10    end
11   
12    def title
13      @product.title
14    end
15   
16    def price     #计算总价
17      @product.price*@quantity
18    end
19  end
对models/cart.rb修改add_product()方法

1  def add_product(product)
2      current_item=@items.find{|item| item.product==product}  #查看物品列表中是否已经有了product
3      if current_item                           #如果已经存在
4        current_item.increment_quantity         #物品数量+1
5      else                                    #如果不存在
6        @items<<CartItem.new(product)        #新增一个CartItem对象
7      end
8    end
另外,对add_to_cart视图做一个简单修改,将“数量”显示出来
1  <h1>Your Pragmatic Cart</h1>

3  <ul>
4      <%for cart_item in @cart.items%>
5        <li><%=cart_item.quantity%> &times; <%=h(cart_item.title)%></li>
6      <%end%>
7   
8  </ul>
这时会跳出一个错误页面来
原因:程序认为购物车中的东西是Product对象,而不是CartItem对象,好像Rails压根就不知道已经修改了Cart类。
购物车对象从session中来,而session中的购物车对象还是旧版本的:它直接把货品放进@items数组,所以当Rails从session中取出这个购物车之后,里面装的都是Product对象,而不是CartItem对象。
解决方法:删除旧的session,把原来那个购物车留下的所有印记都抹掉,由于我们使用数据库保存session数据,只要一个rake命令就可以清空session表。
Rake db:sessions:clear
现在执行Rails应用。
迭代C3.处理错误
问题描述:现在有一个程序bug当在浏览器中敲入[url]http://.../store/add_to_cart/id[/url],id如果不在货品id范围内,会引发一个错误页面。
解决途径:
Flash结构—所谓flash其实就是一个篮子(与hash非常类似),你可以在处理请求的过程中把任何东西放进去。在同一个session的下一个请求中,可以使用flash的内容,然后这些内容就会被自动删除。一般来说,flash都是用于收集错误信息的,譬如:当add_to_cart() action发现传入的id不合法时,它就会将错误信息保存在flash中,并重定向到index() action,以便重新显示分类列表页面。Index action的视图可以将flash中的错误信息提取出来,在列表页面顶端显示。在视图中,使用flash方法即可访问flash中的信息
不用普通实例变量保存错误信息的原因:重定向指令是由应用程序发送给浏览器,后者接收到该指令后,再向应用程序发送一个新的请求。当应用程序收到后一个请求时,前一个请求中保存的实例变量早就消失无踪了。Flash数据是保存在session中的,这样才能够在多个请求之间传递。
对add_to_cart()方法进行修改,拦截无效的货品id,并报告这一错误:
controller/store_controller.rb
1    def add_to_cart
2      begin
3        product=Product.find(params[:id])
4      rescue ActiveRecord ::RecordNotFound       # rescue 子句拦下了 Product.find()抛出的异常
5        logger.error("Attempt to access invalid product #{params[:id]}")  #写入日志,级别是error
6        flash[:notice] = "Invalid product"  #创建flash信息解释出错的原因,跟hash一样,:notice作为键
7        redirect_to :action => "index" #让浏览器重新定向
8      else         #没有异常发生时
9        @cart=find_cart
10        @cart.add_product(product)
11      end
12    end
启动Rails应用看一下结果,当在浏览器后面不是货品里的id时,代码就会生效,日志里可以查找到Attempt to access invalid product xx。
这里日志生效了,但是没有把flash信息显示在浏览器里。
在layouts中修改store.rhml
1    <div id="main">
2              <%if flash[:notice]%>      #如果flash里有内容
3                  <div id="notice"><%=flash[:notice]%></div>      #显示flash的内容
4              <%end%>
5              <%= yield :layout %>
6    </div>
迭代C4:结束购物车
任务:加入清空购物车功能,用一个button_to()方法添加一个empty_cart()方法
add_to_cart.rhtml加入下面语句
<%=button_to "Empty cart", :action=>:empty_cart %>
这里有:action=>:empty_cart, empty_cart()这个action要实现的功能是将购物车从session中去掉,并在flash中设置提示信息,最后将浏览器重新定向到首页。
store_controller.rb
1  def empty_cart()
2      session[:cart]=nil                        #清空购物车
3      flash[:notice]="Your cart is currently empty"       #flash中设置提示信息
4   redirect_to :action => "index"                   #重新定向首页
5  end
在store_controller.rb中注意到有两次调用到redirect_to方法,两次使用flash的内容显示信息。为了不出现重复代码可以修改一下
1    def add_to_cart
2      begin
3        product=Product.find(params[:id])
4      rescue ActiveRecord::RecordNotFound
5        logger.error("Attempt to access invalid product #{params[:id]}")
6        redirect_to_index("Invalid product")
7      else
8        @cart=find_cart
9        @cart.add_product(product)
10      end
11    end
12 
13    def empty_cart()
14      session[:cart]=nil
15      redirect_to_index("Your cart is currently empty")
16    end
17    private
18    def redirect_to_index(msg)
19      flash[:notice]=msg
20      redirect_to :action => "index"
21    end
总结:
    使用session来存储状态
    创建并使用了与数据库无关的模型对象
    用flash在action之间传递错误信息
    用日志器记录事件
    用辅助方法消除重复代码
任务D:Ajax初体验
--Ajax介绍:
 Ajax不是一种技术。实际上,它由几种蓬勃发展的技术以新的强大方式组合而成。Ajax包含:
基于XHTML和CSS标准的表示;
使用Document Object Model进行动态显示和交互;
使用XMLHttpRequest与服务器进行异步通信;
使用JavaScript绑定一切。
 
 Ajax是由Jesse James Garrett创造的,是“Asynchronous JavaScript + XML的简写”。
 Ajax用来描述一组技术,它使浏览器可以为用户提供更为自然的浏览体验。在Ajax之前,Web站点强制用户进入提交/等待/重新显示范例,用户的动作总是与服务器的“思考时间”同步。Ajax提供与服务器异步通信的能力,从而使用户从请求/响应的循环中解脱出来。借助于Ajax,可以在用户单击按钮时,使用JavaScript和DHTML立即更新UI,并向服务器发出异步请求,以执行更新或查询数据库。当请求返回时,就可以使用JavaScript和CSS来相应地更新UI,而不是刷新整个页面。最重要的是,用户甚至不知道浏览器正在与服务器通信:Web站点看起来是即时响应的。
  虽然Ajax所需的基础架构已经出现了一段时间,但直到最近异步请求的真正威力才得到利用。能够拥有一个响应极其灵敏的Web站点确实激动人心,因为它最终允许开发人员和设计人员使用标准的HTML/CSS/JavaScript堆栈创建“桌面风格的(desktop-like)”可用性。
  所有这些Web站点都告诉我们,Web应用程序不必完全依赖于从服务器重新载入页面来向用户呈现更改。一切似乎就在瞬间发生。简而言之,在涉及到用户界面的响应灵敏度时,基准设得更高了。
Ajax的工作原理
  Ajax的核心是JavaScript对象XmlHttpRequest。该对象在Internet Explorer 5中首次引入,它是一种支持异步请求的技术。简而言之,XmlHttpRequest使您可以使用JavaScript向服务器提出请求并处理响应,而不阻塞用户。
  在创建Web站点时,在客户端执行屏幕更新为用户提供了很大的灵活性。下面是使用Ajax可以完成的功能:
    动态更新购物车的物品总数,无需用户单击Update并等待服务器重新发送整个页面。
    提升站点的性能,这是通过减少从服务器下载的数据量而实现的。例如,在Amazon的购物车页面,当更新篮子中的一项物品的数量时,会重新载入整个页面,这必须下载32K的数据。如果使用Ajax计算新的总量,服务器只会返回新的总量值,因此所需的带宽仅为原来的百分之一。
    消除了每次用户输入时的页面刷新。例如,在Ajax中,如果用户在分页列表上单击Next,则服务器数据只刷新列表而不是整个页面。
    直接编辑表格数据,而不是要求用户导航到新的页面来编辑数据。对于Ajax,当用户单击Edit时,可以将静态表格刷新为内容可编辑的表格。用户单击Done之后,就可以发出一个Ajax请求来更新服务器,并刷新表格,使其包含静态、只读的数据。
  一切皆有可能!但愿它能够激发您开始开发自己的基于Ajax的站点。然而,在开始之前,让我遵循传统的提交/等待/重新显示的方式,然后讨论Ajax如何提升用户体验。
给购物车加上Ajax:
任务:把当前购物车显示到分类页面的边框里,而不要一个单独的购物车页面,然后加上Ajax魔法,只更新边框里的购物车,而不必重新刷新整个页面。
…要使用Ajax,先做一个非Ajax版本的应用程序,然后逐步引入Ajax功能。
迭代D1.迁移购物车
任务:将原先由add_to_cart和对应的rhtml模板来渲染的购物车的逻辑放到负责分类页面的布局模板中。使用局部模板
add_to_cart.rhtml
1  <div class="cart-title">Your Cart</div>
2  <table>
3       #render()方法接收两个参数,局部模板的名字和一组对象的集合
4      <%=render(:partial=>"cart_item", :collection=>@cart.items)%>  #cart_item局部模板名
5        <tr class="total-line">         #tr指定表格中的一行。
6          <td colspan="2">Total</td>         #td指定表格中的单元格。‘2’代表2个单元格
7          <td class="total-cell"><%=number_to_currency(@cart.total_price)%></td>
8        </tr>
9  </table>
10   
11  <%=button_to "Empty cart", :action=>:empty_cart %>
局部模板本身就是另一个模板文件(默认情况下与调用它的模板文件位于同一个目录下)。为了从名字上将局部模板与普通模板区分开来,Rails认为局部模板的名字都以下划线开头,在查找局部模板时也会在传入的名字前面加上下划线,所以,我们的局部模板更add_to_cart.rhtml在同一目录下,名字是_cart_item.rhtml
1  <tr>    #指定表格中的一行
2      <td><%=cart_item.quantity%>&times;</td>      #显示为2x
3      <td><%=h(cart_item.title)%></td>          #显示货品的title
4      <td class="item-price"><%=number_to_currency(cart_item.price)%></td>
5  </tr>
注意:局部模板的名字是”cart_item”,所以在局部模板内部就会有一个名叫”cart_item”的变量。
现在已经对“显示购物车”的代码做出了整理,但还没有将它搬到边框里。要实现这次搬迁,就需要将这部分逻辑放到局部模板中。如果我们编写出一个用于显示购物车的局部模板,那么只要在边框里嵌入如下调用就可以了:render(:partial=>"cart")
但局部模板怎么知道哪里去获得购物车对象呢?还记得在add_to_cart 模板中用到的“渲染一组对象”的技巧么?给局部模板内部的cart_item变量设上了值。当直接调用局部模板时也可以做同样的事情:render()方法的:object参数接收一个对象作为参数,并将其赋给一个与局部模板同名的变量。所以在局部模板中,我们可以这样调用render()方法:
<%=render(:partial=>"cart", :object=>@cart)%>
这样在_cart.rhtml模板中就可以通过cart变量来访问购物车。
现在创建_cart.rhtml模板,它和add_to_cart模板大同小异,不过使用了cart变量而不是@cart变量。(注意:局部模板还可以调用别的局部模板)
_cart.rhtml
1  <div class="cart-title">Your Cart</div>
2  <table>
3    <%=render(:partial=>"cart_item", :collection=>cart.items)%>
4      <tr class="total-line">
5        <td colspan="2">Total</td>
6        <td class="total-cell"><%=number_to_currency(cart.total_price)%></td>
7        </tr>
8      </table>

10  <%=button_to "Empty cart", :action=>:empty_cart %>
现在修改store布局模板,在边框中加上新建的局部模板。

1  <div id="side">
2            <div id="cart">
3            <%=render(:partial=>"cart", :object=>@cart)%>
4            </div>
5            <a href="http://www....">Home</a><br />

现在我们必须对storeController稍加修改,因为在访问index这个action时会调用布局模板,但这个action并没有对@cart变量设值。
store_controller.rb
1   def index
2      @products=Product.find_products_for_sale
3      @cart = find_cart
4    end
现在查看一下页面
问题:点了 Add to Cart按钮就跳转到add_to_cart这个页面,不利于继续购物。
改变流程
购物车已经在边框中显示出来了,随后我们改变”add to cart”按钮的工作方式:它无须显示一个单独页面,只要刷新首页就行了。改变方法:在add_to_cart这个action方法的最后,直接把浏览器重定向到首页。
1    def add_to_cart
2      begin
3        product=Product.find(params[:id])
4      rescue ActiveRecord::RecordNotFound
5        logger.error("Attempt to access invalid product #{params[:id]}")
6        redirect_to_index("Invalid product")
7      else
8        @cart=find_cart
9        @cart.add_product(product)
10        redirect_to_index
11      end
12    end
为了能让这段代码工作,还需要修改def redirect_to_index()的定义,将消息这个参数变成可选的。
1    def redirect_to_index(msg=nil)
2      flash[:notice]=msg if msg
3      redirect_to :action => "index"
4    end
是时候去掉add_to_cart.rhtml模板了!
问题:这里用了重新定向页面,如果列表页面很大的话,会占用带宽和服务器资源。解决方案Ajax!
迭代D2.基于 Ajax的购物车
Ajax允许编写的代码在浏览器中运行,并与服务器的应用程序交互,在这里,让”Add to Cart”按钮在后台调用服务器add_to_cart方法,随后服务器把关于购物车的HTML发回浏览器,我们只要把服务器更新的HTML 片段替换到边框里就行了。
要实现这种效果,通常做法是首先编写在浏览器中运行的JavaScript代码,然后编写服务器代码与Javascript交互(可能是通过JSON之类的技术)。Good news:只要使用Rails,这些东西都将被隐藏起来:我们使用Ruby(再借助一些Rails辅助方法)就可以完成所有功能。
向应用程序中引入Ajax的技巧在于“小步前进”,所以我们先从最基本的开始:修改货品列表页,让它向服务器端应用程序发起Ajax请求:应用程序则应答一段HTML代码,其中展示了最新的购物车。
在索引页上,目前我们使用button_to()来创建“Add to cart”链接的。button_to()其实就生成了HTML的<form>标记。下列辅助方法
<%=button_to "Add to Cart", :action=>:add_to_cart, :id=>product%>
会生成类似这样的HTML代码:
<form method="post" action="/store/add_to_cart/1" class="button-to">
<div><input type="submit" value="Add to Cart" /></div>
</form>
这是一个标准的HTML表单,所以,当用户点击提交按钮时,就会生成一个post请求。我们不希望这样,而是希望它发送一个Ajax 请求。为此,必须直接地编写表单代码—可以使用form_remote_tag这个Rails辅助方法。”form_..._tag”这样的名字代表它会生成HTML表单,”remote”则说明它会发起Ajax远程调用。现在打开index.rhtml,将button_to()调用替换为下列代码:
1     <% form_remote_tag :url => {:action => :add_to_cart,:id=>product} do%>
2      <%=submit_tag "Add to Cart"%>
3     <%end%>
#用:url参数告诉form_remote_tag()应该如何调用服务器端的应用程序。该参数接收一个hash,其中的值就跟传递给button_to()方法的参数一样。在表单内部(也就是do和end之间的代码块中),我们编写了一个简单的提交按钮。在用户看来,这个页面就跟以前一摸一样。
虽然现在处理的是视图,但我们还需要对应用程序做些调整,让它把Rails需要用到的Javascript库发送到用户的浏览器上。现在需要在store布局的<head>部分里调用javascript_include_tag方法即可。
views/layouts/store.rhtml
1    <head>
2      <title>Pragprog Books Online Store</title>
3      <%= stylesheet_link_tag "depot", :media => "all" %>
4      <%= javascript_include_tag :defaults%>
5    </head>
浏览器已经能够向应用程序发起Ajax请求,下一步就是让应用程序做出应答。创建一段HTML代码来代表购物车,然后让浏览器把这段HTML代码插入到当前页面的DOM,替换掉当前显示的购物车。为此,要做的第一个修改就是不再让add_to_cart重新定向到首页。

store_controller.rb
   #redirect_to_index
修改的结果是,当add_to_cart完成对Ajax请求的处理之后,Rails就会查照add_to_cart这个模板来执行渲染。但是我们给删掉这个模板了。
.rjs模板可以将Javascript发送到浏览器,需要写的只是服务器端的Ruby代码。
views/store/add_to_cart.rjs

1  page.replace_html ("cart",:partial=>"cart", :object=>@cart)

page这个变量是JavaScript生成器的实例—这是Rails提供的一个类,它知道如何在服务器端创建Javascript,并使其在浏览器上运行。在这里,它找到当前页面上id为cart的元素,然后将其中的内容替换成…某些东西。传递给replace_html的参数看上去很眼熟,因为它们就跟store布局中渲染局部模板时传入的参数完全一样。这个简单的.rjs模板就会渲染出用于显示购物车的HTML代码,随后就会告诉浏览器将id=”cart”的<div>中的内容替换成这段HTML代码。

迭代D3.高亮显示购物车变化
javascript_include_tag辅助方法会把几个JavaScript库加到浏览器中,其中之一effects.js可以给页面装饰以各种可视化效果。其中有“黄渐变技巧”高亮显示。
问题:要标记出其中最近的一个更新。先对cart模型类做调整;让add_product()方法返回与新加货品对应的CartItem对象。
models/cart
1     def add_product(product)
2      current_item=@items.find{|item| item.product==product}
3      if current_item
4        current_item.increment_quantity
5      else
6        current_item = CartItem.new(product)
7        @items<<current_item
8      end
9      current_item
10    end
在store_controller.rb中,我们可以提取这项信息,并将其赋给一个实例变量,以便传递给视图模板。
controllers/store_controller.rb
1  def add_to_cart
2      begin
3        product=Product.find(params[:id])
4      rescue ActiveRecord::RecordNotFound
5        logger.error("Attempt to access invalid product #{params[:id]}")
6        redirect_to_index("Invalid product")
7      else
8        @cart=find_cart
9        @current_item=@cart.add_product(product)
10      end
11    end
在_cart_item.rhtml局部模板中,我们会检查当前要渲染的这种货品是不是刚刚发生了变化,如果是,就给它做个标记:将它的id属性设为currtent_item.
1  <% if cart_item== @current_item%>
2    <tr id="current_item">
3  <%else%>
4    <tr>
5  <%end%>
6    <td><%=cart_item.quantity%>&times;</td>
7    <td><%=h(cart_item.title)%></td>
8      <td class="item-price"><%=number_to_currency(cart_item.price)%></td>
9  </tr>
经过这三个修改之后,最近发生变化的货品所对应的<tr>元素会被打上id=”current_item”的标记。现在,我们只要告诉JavaScript对这些元素施加高亮效果就行了:在add_to_cart.rjs模板中调用visual_effect方法。
1  page.replace_html("cart", :partial => "cart", :object => @cart)

3  page[:current_item].visual_effect :highlight,
4                                    :startcolor => "#88ff88",
5                                    :endcolor => "#114411"
现在浏览页面即可看到效果。
迭代D4.隐藏购物车
 任务:购物车里没有东西的时候不显示购物车。有东西时候才显示购物车
简单做法:当购物车中有东西才包含显示它的那段HTML代码。只需修改_cart一个局部模板
<% unless cart.items.empty?%>

<%end%>
虽然这是一个可行方案,但是却让UI显示有些生硬:当购物车由空转为不空时,整个边框都需要重绘。所以,不这么做,还是做的圆滑一点吧!
Script.aculo.us提供了几个可视化效果,可以漂亮地让页面元素出现在浏览器上。试试blind_down,它会让购物车平滑地出现,并让边框上其他部分依次向下移动。
在add_to_cart模板中实现,因为add_to¬_cart这个模板只有在用户向购物车中添加货品时才会被调用,所以我们知道:如果购物车中有了一件货品,那么就应该在边框中显示购物车了(因为这就意味着此前购物车是空的,因此也是隐藏起来的)。此外,我们需要先把购物车显示出来,然后才使用背景高亮的可视化效果,所以“显示购物车”的代码应该在“触发高亮”代码之前     
add_to_cart.rjs
page.replace_html("cart", :partial => "cart", :object => @cart)     
#START_HIGHLIGHT                        
page[:cart].visual_effect :blind_down if @cart.total_items == 1                
#END_HIGHLIGHT
page[:current_item].visual_effect :highlight,
                                  :startcolor => "#88ff88",
                                  :endcolor => "#114411"
这里@cart.total_items,total_item方法没有定义,在model/cart.rb中定义此方法
  def total_items
      @items.sum{|item| item.quantity}
  end

现在购物车为空的时候还需要将它隐藏起来,其他已经很圆滑的实现了视觉效果。
两个方法:1开始展示的,购物车为空的时候不显示html代码。但是,当用户朝空的购物车放入第一件货品的时候,浏览器会出现一阵闪烁---购物车被显示出来,然后又隐藏起来,然后再被blind_down效果逐渐显示出来
所以,更好的方法2.创建“显示购物车”的html,但对它的CSS样式进行设置,使其不被显示出来—如果购物车为空,就设置为display:none.为此,需要修改layouts/store.rhtml文件,首先的方法:

<div id="cart"
          <%if @cart.items.empty?%>
            style="display:none"
          <%end%>
>
          <%=render(:partial=>"cart", :object=>@cart)%>
当购物车为空时,这段代码就会给<div>标记加上style="display:none"这段CSS属性。这确实有效,不过真的很丑陋,下面挂着一个孤零零的”>”字符,看起来就像放错了地方(虽然确实没放错);更糟糕的是,逻辑放到了HTML标记的中间,这正是给模板语言带来恶名的行为。不要让这么丑陋的代码出现,我们还是创建一层抽象—编写一个辅助方法把它隐藏起来吧!
辅助方法
每当需要将某些处理逻辑从视图(不管是哪种视图)中抽象出来时,我们就可以编写一个辅助方法。
在app目录下,有一个helpers的子目录,我们的辅助方法就放在这个子目录下,Rails代码生成器会自动为每个控制器(这里就是store和admin)创建一个辅助方法文件;Rails命令本身(也就是一开始创建整个应用程序的命令)则生成了该控制器引用的视图都可以使用该方法;如果把方法放在application_helper.rb文件中,则整个应用程序的所有视图都可以使用它。所以我们就需要给新建的辅助方法找一个安身之所了:目前还只有store控制器的视图中需要使用它,所以就放在store_helper.rb文件中吧。
在store_helper.rb这个文件中,默认代码是:
module StoreHelper
end
我们来编写一个名叫hidden_div_if()的辅助方法,它接收两个参数:一个条件判断,以及(可选的)一组属性。该方法会创建一个<div>标记,如果条件判断为true,就给它加上display:none样式。在布局文件中,我们这样使用它:/layouts/store.rhtml:
<div id="columns">
    <div id="side">
    <% hidden_div_if(@cart.items.empty?, :id => "cart") do %>
      <%= render(:partial => "cart", :object => @cart) %>
    <% end %>
   
      <a href="http://www....">Home</a><br />
这里的辅助方法hidden_div_if()写在store_helper.rb中,这样就只有storeController能够使用它。
module StoreHelper
 
  def hidden_div_if(condition, attributes={})
    if condition
      attributes["style"]="display: none"
    end
    attrs=tag_options(attributes.stringify_keys)
    "<div #{attrs}>"
  end
end
#我们从Rails提供的content_tag()辅助方法中抄袭了一段代码,才知道可以这样调用tag_options()方法。
目前当用户清空购物车时,会显示一条flash消息。现在要把这条消息去掉了—我们不再需要它了!因为当货品列表页刷新时,购物车直接就消失了。除此之外,还有另一个原因要去掉它:我们已经用Ajax来向购物车中添加货品,用户在购物时主页面就不会刷新;也就是说,只要用户清空了一次购物车,那条flash消息就会一直显示在页面上,告诉他“购物车已经被清空”,即便边框里显示的购物车又被放进了新的货品。
Store_controller.rb

  def empty_cart()
    session[:cart]=nil
    redirect_to_index
  end
看起来步骤不少,其实不然:要隐藏和显示购物车,我们所需要做的只是根据其中货品的数量设置css的显示样式,另外,当第一件货品被放进购物车时,通过.rjs模板来调用blind_down效果,仅此而已。
迭代D5:JavaScript被禁用的对策
如果针对add_to_cart的请求不是来自JavaScript,我们就希望应用程序仍然采用原来的行为,并将浏览器重新定向到首页。当首页显示出来时,更新之后的购物车就会出现在边栏上了。
当用户点击form_remote_tag中的按钮时,可能出现两种不同情况。如果JS被禁用,表单的目标action就会被一个普通的HTTP POST请求直接调用—这就是一个普通表单;如果允许使用JS,它就不会发起普通POST调用,而由一个JS对象和服务器建立后台连接—这个JS对象是XmlHTTPRequest的实例,由于这个类的名字拗口,很多人(以及Rails)把它简称为xhr。
所以,我们可以在服务器上检查进入的请求是否由xhr对象发起,从而判断浏览器是否禁用了JS。Rails提供的request对象—在控制器和视图中都可以访问这个对象—让你可以方便地做这个检查:调用xhr?方法就可以了,这样,只要在add_to_cart中加上一行代码,不管用户的浏览器是否允许使用JS,我们的应用程序就都能支持了。
Controllers/store_controller.rb
def add_to_cart
    begin                    
      product = Product.find(params[:id]) 
    rescue ActiveRecord::RecordNotFound
      logger.error("Attempt to access invalid product #{params[:id]}")
      redirect_to_index("Invalid product")
    else
      @cart = find_cart
     
      @current_item = @cart.add_product(product)
     
      redirect_to_index unless request.xhr?
    end
  end
总结:
    把购物车搬到边栏里,并且让add_to_cart这个action重新显示货品分类页面
    用form_remote_tag()来调用add_to_cart,向它发起Ajax请求。
    我们利用.rjs模板,单独更新购物车那一小块HTML
    为了让用户更加清晰的看到购物车的变化,我们加上了高亮显示的效果—仍然是用.rjs模板实现的
    编写了一个辅助的方法,当购物车为空时将其隐藏,添加货品时再用.rjs模板将其显示出来
    最后,让应用程序在禁用JS的浏览器上也能工作—此时它将采取没有加入Ajax之前的行为方式
渐进式的Ajax开发方式:首先从一个传统的应用程序开始,逐渐向其中添加Ajax的特性,Ajax可能很难测试;如果逐渐添加Ajax特性,当遇到困难时,也可以比较容易的找出问题所在。另外,从一个传统的应用程序开始也使你能够更容易的同时支持Ajax和非Ajax的行为方式。
最后,如果打算进行大量的Ajax开发,应该熟悉浏览器上的JS和DOM监视工具。Pragmatic Ajax:A web 2.0 Primer一书第八章提供了很多有用的提示。开发过程中同时运行两个不同的浏览器,一个允许JS,另一个禁用。当添加新特性时用这两个浏览器来检查,以确保不管是否允许使用JS都能使用这些功能。
任务E:付账
迭代E1:收集订单信息
订单(order)是由一组订单项(line item)、外加购买交易的详细信息构成的。订单项已经初具雏形了,那就是购物车重的物品。不过现在还没有一张数据库表来存储订单项,同样也没有保存订单信息的表,现在可以通过手动生成Rails模型,并用迁移任务来创建对应的表。
depot>ruby script/generate model order
depot>ruby script/generate model line_item
005_create_orders.rb
class CreateOrders < ActiveRecord::Migration
  def self.up
    create_table :orders do |t|
      t.column :name, :string
      t.column :address, :text
      t.column :email, :string
      t.column :pay_type, :string, :limit=>10
    end
  end

  def self.down
    drop_table :orders
  end
end
006_create_line_items.rb
class CreateLineItems < ActiveRecord::Migration
  def self.up
    create_table :line_items do |t|
      t.column(:product_id, :integer, :null => false)
      t.column(:order_id, :integer, :null => false)
      t.column(:quantity, :integer, :null => false)
      t.column(:total_price, :decimal, :null=> false, :precision=>8, :scale=>2)
    end
    execute "alter table line_items add constraint fk_line_item_products foreign key (product_id) references products(id)"
    execute "alter table line_items add constraint fk_line_item_orders foreign key (order_id) references orders(id)"
  end

  def self.down
    drop_table :line_items
  end
end
在这张表里包含两个外键。这是因为line_items表中的每条记录都需要同时与“订单”和“货品”关联。遗憾的是,Rails迁移任务并未提供一种独立于数据库的方式指定外键约束,所以只好通过执行原始的DDL语句(这里就是Mysql的DDL语句)来建立外键。
实施迁移任务rake db:migrate
模型之间的关系
现在,数据库已经知道订单、订单项和货品之间的关系了,但是Rails应用还不知道。所以,我们需要在模型类中声明一些东西。首先是对model/order.rb,在其中调用has_many()方法。
class Order < ActiveRecord::Base
  has_many :line_items #一个订单(可能)有多个订单项与之关联。这些订单项之所以被关联到这个订单,#是因为它们引用了该订单的id
end

此外,还应该在product模型类中加上has_many声明:如果有很多订单的话,每种货品都有可能有多个订单项与之关联。
/model/product.rb
class Product < ActiveRecord::Base
  has_many :line_items

下面我们就要指定反向的关联:从订单项到订单和货品的关联。为此,我们需要在line_item.rb文件中两次使用belongs_to()声明。
class LineItem < ActiveRecord::Base
  belongs_to :order
  belongs_to :product
end
#belongs_to声明告诉Rails:line_items 表中存放的是orders表和products表中记录的子记录;如果没有对应的订单和货品存在,则订单项不能独立存在。有一种简单的方法可以帮你记住应该在哪里做belongs_to声明:如果一张表包含外键,那么它对应的模型类就应该针对每个外键做belongs_to声明。
这些声明管什么用?简单说,它们会给模型对象加上彼此导航的能力。由于在LineItem模型中添加了belongs_to声明,现在我们就可以直接得到与之对应的Order对象了。
li=LineItem.find(…)
puts “this line item was bought by #{li.order.name}”
另一方面,由于Order类有指向LineItem的has_many声明,我们也可以从Order对象直接引用与之关联的LineItem对象—它们都在一个集合中。
order=Order.find(…)
puts “this order has #{order.line_item.size} line items”
创建表单搜集订单信息
现在数据库表和模型类都已经到位,我们可以开始处理付账的流程了。首先我们需要在购物车里加上一个“Checkout”的按钮,并使其连接到store控制器的checkout方法
views/store/_cart.rhtml

<%=button_to "Checkout", :action=>:checkout%> 
<%= button_to "Empty cart", :action => :empty_cart %>
我们希望checkout这个action能向用户呈现一张表单,提示用户在其中输入将要存入orders表的相关信息:姓名、住址、电子信箱以及付款方式。也就是说,我们要在Rails模板中包含一个表单,表单中的输入字段会关联到Rails模型对象的对应属性,因此需要在checkout方法中创建一个空的模型对象,这样表单中的信息才有地方可以去。(我们还必须找出当前的购物车,因为布局模板需要显示它。每个action开头处都要去查找购物车)
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
 ….
private
 …

现在来看看模板部分,为了搜集用户信息,我们将要使用表单。这里的关键在于:在展示页面时要把初始值填入对应的表单字段;在用户点击“提交”按钮之后又要把这些值取回到应用程序中。
在控制器中,我们将@order 实例变量的值设为一个新建的 Order模型对象。之所以这样做,是因为视图要根据模型对象中的初始值生成表单。乍看上去这没有设么特别的价值—这个新建的模型对象中所有的字段都还没有值,所以表单也是空的。但是猜想别的情况:也许我们需要编辑一个现有的模型对象;或者用户可能已经尝试过输入订单信息,但输入的数据没有通过验证。此时我们就希望模型对象中现存的数据能够填入表单展示给用户。所以,在这个阶段传入一个空的模型对象就可以让所有这些情况统一起来—视图可以始终认为有一个可用的模型对象。
随后,当用户点击“提交”按钮时,我们希望在控制器中将来自表单的新数据取回到模型对象中。
Rails使这一任务变得易如反掌:它提供了一组用于处理表单的辅助方法,这些辅助方法会与控制器与模型对象交互,实现了完整的表单处理功能。
先来看个例子:
<%form_for :order, :url=> {:action=>:save_order} do |form|%>
  <p>
    <label for="order_name">Name: </label>
    <%=form.text_field :name, :size=>40%>
  </p>
<%end%>
第一行的form_for辅助方法搭建了一个标准的HTML表单。传入的第一个参数:order告诉该方法:它正在处理的是来自order实例变量的对象。辅助方法会根据这一信息给字段命名,并安排如何将字段的输入值传回给控制器。
:url参数会告诉辅助方法,当用户点击“提交”按钮时应该如何操作。在这里,我们会生成一个HTTP POST请求,并把请求发送给控制器中的save_order方法。
可以看到,form_for实际上搭建了一个Ruby代码块环境(这个代码块在第六行结束)。在代码块内部,你可以放入普通的模板内容(例如<p>标记),同时也可以使用代码块的参数(也就是这里的form变量)来引用表单上下文环境。在第4行,我们就用上了这个上下文环境来向表单中添加文本字段。由于这个文本字段是在form_for的上下文环境中构造出来的,因此它就自动地与@order对象中的数据建立起关联了。
 
form_for中的名字映射到了模型对象以及其中的属性
Rails需要知道每个字段的名称和值,这样才能将其与模型对象关联;而form_for和各种字段层面的辅助方法(例如text_field)正是用于提供这些信息的。上图展示了这个过程。
现在可以创建一个模板,把上述“搜集用户信息”的表单放入其中。这个模板将被store控制器的checkout方法调用,所以它的名字应该是checkout.rhtml,位于view/store目录下。
Rails为各种HTML层面的表单元素提供对应的表单辅助方法。在下面代码中,我们用到text_field和text_area辅助方法来搜集顾客的姓名、电子邮件和居住地址。
<div class="depot-form">
  <%=error_messages_for 'order' %>
 
  <fieldset>
    <legend>please Enter Your Details</legend>
   
    <% form_for(:order, :url=>{:action=>:save_order}) do |form|%>
      <p>
        <label for="order_name">Name:</label>
        <%=form.text_field(:name, :size=>40)%>
      </p>
   
      <p>
        <label for="order_address">Address:</label>
        <%=form.text_area(:address, :rows=>3, :cols=>40)%>
      </p>
      <p>
        <label for="order_email">E-Mail:</label>
        <%= form.text_field(:email, :size=>40)%>
      </p>
      <p>
        <label for="order_pay_type">Pay with:</label>
        <%= form.select :pay_type,
          Order::PAYMENT_TYPES,
          :prompt=>"Select a payment method"
        %>
      </p>
      <%=submit_tag " Place Order", :class=>"submit"%>
    <%end%>
  </fieldset>
 
</div>
#注意与下拉表框相关的代码。我们首先假设Order模型类提供了一组可选的付款方式—在模型类中这应该是一个数组,其中的每一个元素又是一个数组:当第一个元素是一个字符串,代表将在下来列表框中显示的文本;第二个元素是要存入数据库的值。现在就动手在order.rb中定义这个数组吧!
models/order.rb
class Order < ActiveRecord::Base
  has_many :line_items
  PAYMENT_TYPES=[
    #Displayed       stored in db
    ["Check", "check"],
    ["Credit card", "cc"],
    ["Purchase order", "po"]
  ]
end
在这个模板中,我们把“付款方式”的数组传给select辅助方法。同时传入的还有 :prompt参数,它会给下来列表添加一个空的选项,在其中显示提示文本。
再加上一点CSS魔法,这个表单就基本到位了。
在浏览器中检查一下。看起来不错,如果点击“place order”按钮,将会出现这个错误
    Unknown action
No action responded to save_order
不过在写这个action之前,还是加上逻辑验证!
models/order.rb
class Order < ActiveRecord::Base
  has_many :line_items
  PAYMENT_TYPES=[
    #Displayed       stored in db
    ["Check", "check"],
    ["Credit card", "cc"],
    ["Purchase order", "po"]
  ]
 
  validates_presence_of :name, :address, :email, :pay_type
  validates_inclusion_of :pay_type, :in=>PAYMENT_TYPES.map
    {|disp, value| value}
end
搜集订单详细信息
现在来实现store控制器中的save_order()方法吧,这个方法需要:
1.    搜集订单中的数据,填入一个新的Order模型对象
2.    将购物车中的物品填入订单
3.    校验并保存订单,如果校验失败,则显示对应的错误消息,并让用户更正错误。
4.    订单成功保存之后,重新显示货品列表页面,其中包含一条“已经成功下单”的提示信息
所以这个方法的代码如下:
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对象。随后的一行将购物车中的物品放入表单—稍后我们会实现这个方法。在第五行上,我们要求这个Order对象将它自己(以及所有子对象,也就是LineItem对象)存入数据库。在这个过程中,Order对象会执行数据校验(不过我们还要等会儿再加上数据校验)。如果保存成功,我们会做两件事。首先,将购物车从session中删掉,准备接受顾客的下一次购买;然后,用redirect_to_index()方法重新显示货品列表页面,并在上面显示一条提示信息。如果保存失败,则重新显示付账的表单。
在save_order方法中,我们假设Order对象已经提供了add_line_items_from_cart()方法,所以现在就需要实现这个方法。
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
可以看到,我们不需要做任何额外的工作来处理外键,例如在订单中设置order_id字段以便引用新建的订单之类。Rails已经在has_many()和belong_to()声明中帮我们做好了工作。我们在第4行里往line_items集合中新增一个订单项,Rails就会负责帮我们处理外键关联。

Order类中的这个方法又会用到LineItem模型类中的一个简单的辅助方法,该方法会根据购物车中的物品新建一个订单项。
models/line_item.rb
class LineItem < ActiveRecord::Base
  belongs_to :order
  belongs_to :product
 
  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
end
在浏览器中测试一下,进入付账页面,不填写任何字段,直接点击”place order” 按钮。此时系统应该重新显示付账页面,并且给出错误信息,提示哪些字段尚未填写完成。
在提交了订单信息后,登陆mysql查看表单:
>mysql –u root depot_development
mysql> select * from orders;
+----+-------+---------+-------+----------+
| id | name  | address | email | pay_type |
+----+-------+---------+-------+----------+
|  1 | china | dp      | ilm   | cc       |
+----+-------+---------+-------+----------+
1 row in set (0.00 sec)

mysql> select * from line_items;
+----+------------+----------+----------+-------------+
| id | product_id | order_id | quantity | total_price |
+----+------------+----------+----------+-------------+
|  1 |    7    |    1  |      1 |     29.95 |
+----+------------+----------+----------+-------------+
1 row in set (0.00 sec)
最后一点Ajax修改
在接受订单之后,我们将用户重新定向回到首页,并显示一条flash信息:“Thank you for your order.”如果用户继续购物,并且又允许 JS在浏览器中运行,那么购物车会在页面的边栏里显示,往其中放入货品也不会重绘主页面—也就是说,这条flash信息会一直显示在页面上,相比之下,我们更希望在顾客往购物车中放入货品之后让这条信息消失(如果浏览器禁用JS,效果就是这样)。弥补方法很简单:只要在选购物品之后把包含flash信息的<div>隐藏起来即可。
首先想到的办法是在add_to_cart.rjs中加上这么一行:
page[:notice].hide
#rest as before…
可这不管用。当我们首次进入在线商店时,flash中什么都没有,所以id叫notice的这个<div>也不会显示出来;由于没有这么一个<div>,rjs模板生成的JS在尝试隐藏这个<div>时就会出错,剩下的脚本就不会运行了。结果是,我们将再也无法看到边栏中的购物车被更新。
最终的解决方案有些类似于hack:只有当这个<div>存在时,我们才想运行.hide方法。但rjs并没有提供这种“检查div是否存在”的功能。不过rjs允许我们遍历页面上所有与指定的CSS selector模式匹配的元素,所以我们不妨遍历所有id为notice的<div>标记。在遍历的过程中我们可能找到一个元素,并会把它隐藏起来;也可能一个元素也找不见,此时就不会调用hide方法。
views/store/add_to_cart.rjs
page.select("div#notice").each { |div| div.hide }

page.replace_html("cart", :partial => "cart", :object => @cart)
        
#START_HIGHLIGHT                        
page[:cart].visual_effect :blind_down if @cart.total_items == 1                
#END_HIGHLIGHT

page[:current_item].visual_effect :highlight,
                                  :startcolor => "#88ff88",
                                  :endcolor => "#114411"
总结:
    创建了orders和line_items两张表(以及对应的模型类),并将它们连接起来
    创建了一张表单,用于搜集订单详细信息,并将其与Order模型类关联起来
    添加了数据校验,并利用辅助方法将错误信息回显给用户
任务F:管理
迭代F1:添加用户
先来创建一张简单的数据库表,用于保存用户名和经过加密的密码,以便管理之用。我们不能以明文形式保存密码,而是要首先对其进行SHA1加密,然后保存一个160位的散列码。当用户再次登陆时,我们会对她输入的密码做同样的加密处理,并将加密的结果与数据库中保存的散列码进行比较。为了让系统更加安全,我们还对密码进行了salt处理:当生成散列值时将密码与一个伪随机字符串组合之后再生成散列码。
depot>ruby script/generate model user
数据迁移任务:007_create_users.rb
class CreateUsers < ActiveRecord::Migration
  def self.up
    create_table :users do |t|
      t.column(:name, :string)
      t.column(:hashed_password, :string)
      t.column(:salt, :string)
    end
  end

  def self.down
    drop_table :users
  end
end
depot>rake db:migrate
加上数据校验
models/user.rb
class User < ActiveRecord::Base
  validates_presence_of :name       #用户名不为空
  validates_uniqueness_of :name        #用户名唯一
 
  attr_accessor :password_confirmation
  validates_confirmation_of :password
 
  def validate
    errors.add_to_base("missing password") if hashed_password.blank?
  end
end
输入密码的表单,密码输入两次,验证两次密码是否相同。Rails可以自动的校验这两遍输入的密码是否相同。最后,我们用一个校验钩子来检查密码已经被设上了值,但我们不会直接检查password属性。为什么?因为这个属性并不真的存在—至少不存在于数据库里。所以,我们需要检查它的替身—也就是经过散列加密的密码—存在。
第一个问题:如何得到密码的散列值?这里的关键在于生成一个唯一的salt值,将其与密码明文组合成一个字符串,然后对这个字符串进行SHA1加密,得到一个40字符的字符串(其中的内容是一个十六进制数)。我们把这一逻辑写在一个私有的类方法中。
models/user.rb
  private
 
  def self.encrypted_password(password,salt)
    string_to_hash=password+"wibble"+salt #'wibble makes it harder'
    Digest::SHA1.hexdigest(string_to_hash)
  end
将一个随机数与对象ID组合起来,就得到了我们需要的salt字符串—salt值是什么并不重要,只要它是无法预测的就行(譬如说,用时间作为salt值的熵【混乱度】就不如用随即字符串来得高)。最后把这个salt值放进模型对象的salt属性中。这是个private方法
models/user.rb
  def create_new_salt
    self.salt=self.object_id.to_s+rand.to_s
  end
代码里写self.salt=…,以强制调用salt=方法—准确的说是“调用当前对象的salt=方法”。如果没有self.的话,Ruby认为是在给一个局部变量赋值,那代码就没有效果了。
还需要点代码,确保每当明文密码放进User对象时,都会自动生成密码后的散列码(后者将被存入数据库)。为此,我们将把“明文密码”变成模型中的一个虚拟属性—它从应用程序的角度看上去像是个属性,但却不会被存入数据库。
如果不是需要生成散列码,那么只要使用Ruby提供的attr_accessor声明就行了。
attr_accessor :password
但是,自动动手实现访问方法,在写方法中生成一个新的salt值,然后用它来生成密码散列值。
models/user.rb
  def password
    @password
  end
 
  def password=(pwd)
    @password=pwd
    return if pwd.blank?
    create_new_salt
    self.hashed_password=User.encrypted_password(self.password, self.salt)
  end
 
最后一件事:如果用户提供了正确的用户名和密码,我们需要返回一个User对象。由于用户输入的密码是明文,所以我们必须根据用户名取出数据库中的记录,然后根据记录中的salt值再对密码做一次散列处理;如果密码散列值与数据库中保存的值匹配,则返回User对象。现在我们就可以用这个方法来验证用户身份了。
models/user.rb
  def self.authenticate(name, password)
    user=self.find_by_name (name)
    if user
      expected_password=encrypted_password(password, user.salt)
      if user.hashed_password!=expected_password
        user=nil
      end
    end
    user
  end
 #这里使用了一个ActiveRecord小花招:方法第一行调用了find_by_name方法!ActiveRecord会注意到我们调用了一个未经定义的方法,并且该方法的名字以find_by开头,以一个字段名结尾,所以它会自动帮我们创建一个查找方法,并将其添加到模型类上。
user.rb的完整代码:
class User < ActiveRecord::Base
 
  validates_presence_of     :name
  validates_uniqueness_of   :name
 
  attr_accessor :password_confirmation
  validates_confirmation_of :password

  def validate
    errors.add_to_base("Missing password") if hashed_password.blank?
  end

  def self.authenticate(name, password)
    user = self.find_by_name(name)
    if user
      expected_password = encrypted_password(password, user.salt)
      if user.hashed_password != expected_password
        user = nil
      end
    end
    user
  end
 
  # 'password' is a virtual attribute
 
  def password
    @password
  end
 
  def password=(pwd)
    @password = pwd
    return if pwd.blank?
    create_new_salt
    self.hashed_password = User.encrypted_password(self.password, self.salt)
  end
 
  private
 
  def self.encrypted_password(password, salt)
    string_to_hash = password + "wibble" + salt  # 'wibble' makes it harder to guess
    Digest::SHA1.hexdigest(string_to_hash)
  end
 
  def create_new_salt
    self.salt = self.object_id.to_s + rand.to_s
  end
 
end
管理用户
模型和数据库表都已经就位,现在需要通过某种途径来管理这些用户了。为此,添加一些与User对象有关的操作:登陆、列举、删除、新增,等等。为了保存代码干净,我们把这些操作放在一个单独的控制器中。
使用Rails提供的脚手架。不过这次还是亲自动手做吧,尝试一些新鲜的技术。首先新建一个控制器(login),其中包含一组action方法。
depot>ruby script/generate controller login add_user login logout index delete_user list_users
在NB6中,generate->controller:login view: add_user ,login ,logout ,index, delete_user ,list_users
如何在数据库表中新建记录:创建一个action,在视图中放一张表单,然后用这个action来调用这张表单;表单被提交时又会回调某个“保存”action,后者再调用模型对象保存数据。
这里用一种不同的方法创建用户:
在自动生成用于维护products表的脚手架代码中,edit这个action就是用于设置表单以便编辑货品数据的。当用户提交表单时,请求会被分发给控制器中的另一个action: save.这两个方法彼此协作,才完成了这项任务。在搜集订单详细信息时,我们也使用了同样的技巧。
下面是add_user的实现代码:
controllers/login_controller.rb
class LoginController < ApplicationController

  layout "admin"
  def add_user
    @user = User.new(params[:user])
    if request.post? and @user.save
      flash.now[:notice] = "User #{@user.name} created"
      @user = User.new
    end
  end在Rails控制器内,只要通过request属性就可以访问到请求的相关信息,使用get?()和post?()等方法即可检查请求的类型。在这个方法内部,我们会判断收到的请求究竟是要显示出示的空表单呢,还是要将已经填好的表单中的数据存入数据库。解决:只要检查请求所使用的HTTP方法即可。如果请求来自一个<a href=”…”>链接,我们收到的将是一个GET请求,如果请求中包含了表单数据(也就是说,它来自用户点击“提交”按钮),我们就会看到一个POST请求。
首先新建一个User对象。如果参数列表中有用户提交的表单数据,就用它来初始化User对象;如果没有表单数据,则会创建一个空的User对象。如果接收到的是一个GET请求,action的处理就到此结束,立即渲染add_user对应的模板(我们还没编写)。如果接收到的是POST请求,就表示这是用户提交上来的表单,所以应该保存数据。如果保存成功,就把User对象清空,然后重新显示表单(同时友好显示一条flash信息)。让管理员可以继续添加用户。如果保存失败,程序就直接执行了action的结尾处。此时我们手里既有用户输入的无效数据(在@user对象中),又有校验失败的原因(在@user对象的error属性中),所以可以给用户一个更正错误的机会。
在代码中还有一个处理flash信息的技巧:我们希望用普通的flash机制来显示“user added”这样一条信息,但不希望这条信息出现在本次请求之外,所以使用了flash.now这个变量,其中的信息只在当前请求的范围内有效。
创建这个add_user.rhtml的视图
<div class="depot-form">
 
  <%=error_messages_for 'user'%>
 
  <fieldset>
    <legend>Enter User Details</legend>
   
    <%form_for :user do |form|%>
      <p>
        <label for="user_name">Name:</label>
        <%=form.text_field :name, :size=>40%>
      </p>
      <p>
        <label for="user_password">Password:</label>
        <%= form.password_field :password, :size=>40%>
      </p>
      <p>
        <label for="user_password_confirmation">Confirm:</label>
        <%=form.password_field :password_confirmation, :size=>40%>
      </p>
      <%=submit_tag "Add User", :class=>"submit"%>
    <%end%>
  </fieldset>
</div>
现在可以在浏览器中测试这个页面了。[url]http://localhost:3000/login/add_user[/url]
输入用户注册信息(用户名+密码+再输入一次密码),然后通过mysql查询一下:
>mysql –u root depot_development
mysql> select * from users;
+----+----------+------------------------------------------+--------------------
-------+
| id | name     | hashed_password                          | salt
       |
+----+----------+------------------------------------------+--------------------
-------+
|  1 | zllicho  | 1fe72247334778a4e057c9a8fa60b92aac0506ac | 377523500.500789175
067053 |
|  2 | zllicho1 | 4e2748561d4dc735a8bc17257616f91bbde624cb | 386428900.573120445
325151 |
+----+----------+------------------------------------------+--------------------
-------+
2 rows in set (0.06 sec)
迭代F2:登陆
“管理员登陆”功能,具体任务:
    我们提供一张表单,以便管理员输入用户名和密码
    在管理员登陆以后,我们需要在session中记录这一事实,知道他们登出为止
    限制对应用程序中管理端的访问:只允许以管理员身份登陆的用户访问
在LoginController 中添加一个login()方法,该方法要在session中记录一些东西,以证明这里有一个管理员登陆了。不妨把User对象的id写入session,所用的键就是:user_id
controller/login_controller.rb
  def login
    session[:user_id] = nil
    if request.post?
      user = User.authenticate(params[:name], params[:password])
      if user
        session[:user_id] = user.id
        redirect_to(:action => "index")
      else
        flash.now[:notice] = "Invalid user/password combination"
      end
    end
  end
这里使用的技巧就跟add_user()方法一样:同一个方法既可以处理初始的请求,也可以处理来自用户的输入数据。但这里也有新东西:它所使用的表单并非与模型对象直接关联。要明白其中奥妙,还得看看login所使用的模板。
views/login/login.rhtml
<div class="depot-form">
  <fieldset>
    <legend>Please Log In</legend>

    <% form_tag do %>
      <p>
        <label for="name">Name:</label>
        <%= text_field_tag :name, params[:name] %>
      </p>

      <p>
        <label for="password">Password:</label>
        <%= password_field_tag :password, params[:password] %>
      </p>
 
      <p>
        <%= submit_tag "Login" %>
      </p>
    <% end %>
  </fieldset>
</div>
这里没有form_for方法,而是使用了form_tag—后者只是生成一个普通的HTML<form>标记而已。在表单内部又用到了text_field_tag和password_field_tag方法,这两个辅助方法都是用于生成HTML的<input>标记的,他们都接收两个参数:输入字段和名称,以及填入其中的初始值。这样使用表单,让我们可以把params中的值直接与表单输入字段关联起来—不需要任何模型对象作为中介。在这里,我们直接使用了params对象;另外也可以由控制器来提供实例变量给视图使用。
这种表单的工作流程如下图所示,注意表单字段的值是如何通过params在控制器与视图之间流转的:视图从params[:name]中取出值来显示;当用户提交表单时,控制器又可以通过同样的方式得到修改后的值。
如果用户登陆成功了,我们就把用户id保存在session数据里。根据session里是否有这个值的出现,我们就可以判断是否有管理员用户登陆。
 
控制器、模板与浏览器之间的参数流转


最后,该加上index页面了—在这里显示在线商店的订单总数
views/login/index.rhtml
<h1>Welcome</h1>
It's <%= Time.now %>.
We have <%= pluralize(@total_orders, "order") %>.
index()方法则会设置订单数量。
controllers/login_controller.rb]
  def index
    @total_orders = Order.count
  end
迭代F3:访问控制
Rails的过滤器允许对action方法调用进行拦截,在调用action方法之前或之后加上处理逻辑。在这里,要使用前置过滤器(before filter)来拦截所有对admin控制器中action的调用。拦截器将对session[:user_id]进行检查:如果这里有值,并且又能够与数据库中的用户对应上,就说明管理员已经登陆了,调用就会继续进行;如果session[:user_id]没有值,拦截器就会发起一次重新定向,将用户引导到登陆页面。
这个方法应该放在admin控制器里,但是由于某种原因?要放在ApplicationController中—这是所有控制器的父类,同时需要限制对这个方法的访问,因为application.rb中的方法会成为所有控制器的实例方法,其中所有的public方法都会以action的形式暴露给最终用户。
controllers/application.rb
class ApplicationController < ActionController::Base
  session :session_key => '_depot_session_id'

  private

  def authorize
    unless User.find_by_id(session[:user_id])
      flash[:notice] = "Please log in"
      redirect_to(:controller => "login", :action => "login")
    end
  end
end
只要在AdminController中加上一行代码,就可以在调用任何action之前首先调用这个用于身份认证的方法:
controllers/admin_controller.rb
class AdminController < ApplicationController
 
  before_filter :authorize
#…
友好的登陆系统
按照目前代码,如果管理员尝试在未登录状态下访问受限页面,他会被引到登陆页面上;完成登录后,出现统一的状态页面—用户最初请求已经被遗忘了。这里修改一下代码,在用户登陆之后将引到最初请求的页面。
首先,如果authorize()方法需要让用户去登陆的话,应该同时将当前请求的URL记在session中
controllers/application_controller.rb
class ApplicationController < ActionController::Base
  # Pick a unique cookie name to distinguish our session data from others'
  session :session_key => '_depot_session_id'

  private

  def authorize
    unless User.find_by_id(session[:user_id])
      session[:original_url]=request.request_uri
      flash[:notice] = "Please log in"
      redirect_to(:controller => "login", :action => "login")
    end
  end
end
一旦用户登陆成功,可以检查session中是否保存了一个请求URI;如果有的话,就将请求重新定向他原本请求的地址。
controllers/login_controller.rb
def login
    session[:user_id]=nil
    if request.post?
      user=User.authenticate(params[:name],params[:password])
      if user
        session[:user_id]=user.id
        uri=session[:original_url]
        session[:original_url]=nil
        redirect_to(uri || {:action=>"index"})
      else
        flash[:notice]="Invalid user/password combination"
      end
    end
  end
LoginController也需要访问控制,不过即使用户没有登陆,也允许访问login()这个action,所以该方法不应该接受身份认证检查。
controllers/login_controller.rb
class LoginController < ApplicationController

  before_filter :authorize, :except => :login
#…
现在删除session文件:rake db:sessions:clear
测试[url]http://localhost:3000/admin[/url]这时,会出现登陆页面,登陆成功会进入初始请求页面
迭代F4:边栏,以及更多的管理功能
先从边栏开始,由实现order控制器的经验可以知道,现在需要创建一个布局模板。admin控制器的布局模板应该位于layouts/admin.rhtml文件
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
                      "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
  <title>Administer the Bookstore</title>
  <%= stylesheet_link_tag "scaffold", "depot", :media => "all" %>
</head>
<body id="admin">
  <div id="banner">
    <%= p_w_picpath_tag("logo.png") %>
    <%= @page_title || "Pragmatic Bookshelf" %>
  </div>
  <div id="columns">
    <div id="side">
      <p>
        <%= link_to "Products",   :controller => 'admin', :action => 'list' %>
      </p>
      <p>
        <%= link_to "List users", :controller => 'login', :action => 'list_users' %>
        <br/>
        <%= link_to "Add user",   :controller => 'login', :action => 'add_user' %>
      </p>
      <p>
        <%= link_to "Logout",     :controller => 'login', :action => 'logout' %>
      </p>
    </div>
    <div id="main">
    <% if flash[:notice] -%>
      <div id="notice"><%= flash[:notice] %></div>
    <% end -%>
      <%= yield :layout %>
    </div>
  </div>
</body>
</html>
这里看到已经把功能链接放在边栏里了,现在完成这些功能代码:
1.列举用户:
只要在action中把用户列表放进一个实例变量就可以了。
controllers/login_controller.rb
  def list_users
    @all_users=User.find(:all)
  end
然后渲染list_users.rhtml
<h1>Administrators</h1>
<ul>
  <% for user in @all_users %>
    <li><%= link_to "[X]", { # link_to options
                             :controller => 'login',
                             :action => 'delete_user',
                             :id => user},
                           { # html options
                             :method  => :post,
                             :confirm => "Really delete #{user.name}?"
                           } %>
        <%= h(user.name) %>
    </li>
  <% end %>
</ul>
没有管理员了...
删除用户很简单,调用控制器的delete_user的action,同时传入id参数指名要删除哪个用户。delete_user 方法大致如下:
 def delete_user
    if request.post?
      user = User.find(params[:id])

      user.destroy
    end
   redirect_to(:action => :list_users)
  end
这里第二行,检查进入的是否为HTTP POST请求,这是一个好习惯:凡是会改变服务器状态的请求都应该是POST,而不是GET请求。这就是我们修改了表单中link_to的默认设置、让它发起POST请求的原因。不过这项设置只有当用户允许JS时才有效,所以我们要在控制器里加上一个检查:如果真的因为禁用JS而发起了GET请求,就直接忽略该次请求。
现在来测试一下,进入list_users页面,删除唯一用户,删除成功了,现在出现了登陆页面,情形比较尴尬,没有任何管理员,却要求登陆!
还好,我们可以调用script/console命令,Rails会调用irb
E:\Project>cd depot
E:\Project\depot>ruby script/console
./script/../config/boot.rb:43:Warning: require_gem is obsolete.  Use gem instead
.
Loading development environment.
./script/../config/../config/boot.rb:43:Warning: require_gem is obsolete.  Use g
em instead.
>> User.create(:name=>'dave',:password=>'123',:password_confirmation=>'123')
E:/Project/depot/app/models/user.rb:26: warning: don't put space before argument
 parentheses
=> #<User:0x49e7d94 @errors=#<ActiveRecord::Errors:0x49a21a4 @errors={}, @base=#
<User:0x49e7d94 ...>>, @attributes={"salt"=>"387478500.908665942302718", "name"=
>"dave", "hashed_password"=>"55110fd8a9887ca2b4e4cb630995ffa45a671bbc", "id"=>3}
, @password="123", @password_confirmation="123", @new_record=false>
>> User.count
⇨    1
这时麻烦解除了,如何避免这样的事情发生呢?一种方法是A登陆之后不能删除A,但是会碰到这种情况,A登陆了在删除B,而同时B删除了A。
为此,需要用到ActiveRecord的钩子方法,前面已经看到了一个钩子方法:当ActiveRecord要检验模型对象的状态是否合法时,它就会调用该对象的validate方法,实际上,ActiveRecord总共定义了大概20个钩子方法,分别在模型对象生命周期的不同时间点被调用。在这里要使用after_destroy钩子方法,该方法会在delete语句执行之后被调用。由于它与delete语句位于同一个事务中,因此只要在该方法里抛出异常,整个事务就会被回滚。方法实现如下:
models/user.rb
  def after_destroy
    if User.count.zero?
      raise "Can't delete last user"
    end
  end 
这里关键概念是:用异常来表示删除用户的过程中出现了错误。这里的异常同时承担两个任务。首先,在事务内部,异常会导致自动回滚;如果在删除用户之后user表为空,抛出异常就可以撤销删除动作,恢复最后一个用户。
其次,异常可以把错误信息带回给控制器,在控制器中使用begin/rescue 代码块来处理异常,并将错误信息放在flash中报告给用户。
controllers/login_controller.rb
  def delete_user
    if request.post?
      user = User.find(params[:id])
      begin
        user.destroy
        flash[:notice] = "User #{user.name} deleted"
      rescue Exception => e
        flash[:notice] = e.message
      end
    end
    redirect_to(:action => :list_users)
  end
这段代码实际仍存在一个时机问题—只要时机合适,最后两名管理员仍有可能删掉对方的用户。
用户登出
controllers/login_controller.rb
  def logout
    session[:user_id] = nil
    flash[:notice] = "Logged out"
    redirect_to(:action => "login")
  end
功能都齐备了!
检查一下,注意到StoreController中有一点代码重复:除了empty_cart之外,每个action都要到session数据中去寻找用户的购物车,下面这行代码:@cart=find_cart在控制器中出现了好几次,现在可以用过滤器来解决这个问题。于是修改 find_cart()方法,让它直接把找到的结果放进@cart实例变量中。
controllers/store_controller.rb
private
...
  def find_cart
    @cart = (session[:cart] ||= Cart.new)
  end
随后,声明一个前置过滤器,让每个action—除了empty_cart之外—调用之前先调用这个方法。
class StoreController < ApplicationController
 
  before_filter :find_cart, :except=> :empty_cart
...
总结:
    创建了User模型类和对应的数据库表,并对其中的属性进行了校验。User类对用户的密码进行了salt处理和加密,然后将加密得到的散列表存入数据库。还创建了一个虚拟属性,用以代表密码明文。每当密码明文被更改时,User类会自动生成经过加密的散列码。
    手动创建了一个控制器,用于管理用户。研究使用了如果用一个action处理数据更新(在action中用不同的代码来处理HTTP GET请求和POST请求),还使用了form_for辅助方法来渲染表单。
    创建了login这个action,其中用到了另一种表单—没有模型对象与之对应的表单。还看到了参数是如何在视图与控制器之间传递的。
    将一整个应用范围内适用的控制器辅助方法移到ApplicationController类中
    在前置过滤器(before filter)中调用authorize()方法,从而实现了管理端功能的访问控制。
    看到了如何使用script/console(irb)直接与模型类交互
    看到如何借助事务来禁止删除最后一个用户
    用另一个过滤器给控制器中的所有action设置了一个通用的环境。
任务G:最后一片饼干
垃圾邮件群发功能,只要把顾客名单和邮件地址以XML的形式交给它就可以了。
生成XML
首先给应用系统简历一个REST风格的接口。REST是“具备表象的状态迁移”(Representational State Transfer)—用不同的HTTP动作(GET、POST、DELETE等等)在应用程序之间传递请求和应答。这里,市场部的系统要向Depot应用发送一个HTTP GET请求,索取购买了某种货品的顾客详细信息;我们的应用程序则会应答一个XML文档,经过和市场部的IT负责人商议,定下来一个简单的URL格式:
[url]http://my.store.com/info/who_bought/<product[/url] id>
现在有两个问题需要解决:首先找出购买了某种货品的顾客,然后根据顾客列表生成一份XML文档。
首先获取顾客列表!
表间导航
下图展示了目前数据库如何保存订单数据的:每份订单(order)包含多个订单项(line item),每个订单项对应于一种货品(product)。市场部希望用这些关联来导航—不过方向相反:根据货品找到订单项,再通过订单项找到订单。
 
数据库结构
:through关联来实现需求,在Product模型类中添加下列声明:
models/product.rb
class Product < ActiveRecord::Base
  has_many :orders, :through=>:line_items
#...
此前已经用has_many在products和line_items两张表之间建立了父子关系:一件货品对应多个订单项。现在这里又声明说一件货品也会有多份订单产生关联,不过这并不是两张表之间直接的关联,而是要先找到货品对应的所有订单项,然后再找到这些订单项对应的订单。通过had_many :through声明,Rails 就知道了这种简介关联。
当运行上面这段代码时,Rails会生成一段高效的SQL语句来做表间关联,让数据库引擎能够充分优化查询。
有了这句:through声明以后,我们就可以通过Product对象的orders属性来找到与之对应的订单:
product = Product.find(some_id)
orders = product.orders
logger.info(“Product #{some_id} has #{orders.count} orders”)
创建REST接口
单独创建一个控制器,用它来处理这类信息查询的请求
depot>ruby script/generate controller info
  def who_bought
    @product = Product.find(params[:id])
    @orders = @product.orders
  end
现在需要一个模板,用于向调用者返回XML文档。当然也可以用rhtml模板实现这个功能,就像渲染web页面一样,不过,两种更好的办法:1.使用rxml模板,目标就是要简化XML文档的创建。
views/info/who_bought.rxml
xml.order_list(:for_product => @product.title) do
  for o in @orders
    xml.order do
      xml.name(o.name)
      xml.email(o.email)
    end
  end
end
这段Ruby代码使用了Jim Weirich开发的构建器(builder)库,这个库可以根据Ruby代码生成良构的XML文档。
在rxml模板中有一个xml变量,代表构建的XML内容,在这个对象上调用一个方法时(例如模板在第一行调用了order_list()方法),构建器就会生成与方法名对应的XML标记(tag)。如果调用方法的同时传入了一个hash作为参数,这个字符串就会被用做XML标记的值。
如果想生成嵌套的标记,可以在调用外层的构建器方法时传入一个代码块作为参数,这个代码块中创建的XML元素就会被嵌套在外层元素的里面。前例中,就用到了这种方法将一组<order>标记嵌套在<order_list>标记内,然后又在每个<order>标记内嵌套了<name>和<email>这么两个标记。
从浏览器检验上述程序是否有效。在firefox中打开会以语法高亮的形式展示出XML文档。
 
也可以用curl工具,下载地址:[url]http://curl.haxx.se/[/url],选择下载一个OS相应的版本,配置一下环境变量。
>curl [url]http://localhost:3000/info/who_bought/7[/url]
C:\>curl [url]http://localhost:3000/info/who_bought/7[/url]
<order_list for_product="Pragmatic Project Automation">
  <order>
    <name>china</name>
    <email>ilm</email>
  </order>
  <order>
    <name>lee</name>
    <email>[email]ilmf@eyou.com[/email]</email>
  </order>
  <order>
    <name>chinaren</name>
    <email>[email]fjd@china.com[/email]</email>
  </order>
  <order>
    <name>zllicho</name>
    <email>[email]zllichO@eyou.com[/email]</email>
  </order>
</order_list>
实际上,这带来了一个有趣的问题:用同一个action,是否能够让用户通过浏览器访问时看到漂亮的列表页面,同时让哪些通过REST接口发起请求的程序取回XML文档?
分别应答
用户请求都是通过HTTP协议进入Rails应用程序的。一个HTTP消息由头信息以及(可选的)数据(例如来自表单的POST数据)共同组成。一个重要的HTTP头信息是”Accept”,客户端通过它来告诉服务器应该送回什么类型的内容。例如浏览器发送的HTTP请求就可能包含下列头信息:
Accept: text/html, text/plain, application/xml
理论上来说,服务器应该只用这三种类型的内容进行应答。
可以利用这点来编写action,使之对不同的请求报以对应的应答内容。譬如,可以编写who_bought这么个action,并在其中查看客户端接受哪些内容类型。如果客户端只接受XML,我们就应该送回XML格式的REST应答;如果客户端接受HTML,我们就可以渲染一个HTML页面给他。
在Rails中,用respond_to()方法就可以根据不同的接受类型来进行条件处理。首先,来编写一个HTML视图模板:
 view/info/who_bought.rhtml
<h3>People Who Bought <%= @product.title %></h3>

<ul>
  <% for order in @orders  -%>
    <li>
        <%= mail_to order.email, order.name %>
    </li>
  <%  end -%>
</ul>
然后用respond_to()方法根据当前请求的头信息选择适当的模板。
controllers/info_controller.rb
class InfoController < ApplicationController
  
  def who_bought
    @product = Product.find(params[:id])
    @orders  = @product.orders
    respond_to do |format|
      format.html
      format.xml
    end
  end
end
在传递给respond_to的代码块中,列举出了支持的内容类型,像一个case语句,不过最大的区别:各个选项列出的顺序是无所谓的,真正起作用的是请求中的排列顺序(因为客户端需要说明自己首选哪种格式)。
在上述代码中,我们会针对各种内容类型采取默认的行为:对于html,会直接调用render方法,对于xml,会渲染.rxml模板。最终效果就是:对于同一个action,客户端可以选择接受HTML或者XML格式的应答。
可惜的是,很难用浏览器来测试这一功能,所以,需要动用命令行客户端。下面用curl来测试它,curl的-H选项允许指定请求的头信息,先来试试XML
C:\>curl -H "Accept: application/xml" [url]http://localhost:3000/info/who_bought/7[/url]
<order_list for_product="Pragmatic Project Automation">
  <order>
    <name>china</name>
    <email>ilm</email>
  </order>
  <order>
    <name>lee</name>
    <email>[email]ilmf@eyou.com[/email]</email>
  </order>
  <order>
    <name>chinaren</name>
    <email>[email]fjd@china.com[/email]</email>
  </order>
  <order>
    <name>zllicho</name>
    <email>[email]zllichO@eyou.com[/email]</email>
  </order>
</order_list>
再测试一下HTML(重启一下Rails应用)
C:\>curl -H "Accept: text/html" [url]http://localhost:3000/info/who_bought/7[/url]
<h3>People Who Bought Pragmatic Project Automation</h3>

<ul>
      <li>
        <a href="mailto:ilm">china</a>
    </li>
      <li>
        <a href="mailto:ilmf@eyou.com">lee</a>
    </li>
      <li>
        <a href="mailto:fjd@china.com">chinaren</a>
    </li>
      <li>
        <a href="mailto:zllichO@eyou.com">zllicho</a>
    </li>
  </ul>
换种方式请求XML
设置Accept头信息来指定希望得到的内容类型,这是HTTP的“官方”做法,客户端程序并不是总能允许设置头信息的,所以Rails提供了另一种区分内容类型的方式:可以把首选的内容格式写在URL中,譬如,我们希望who_bought请求得到HTML应答,可以请求/info/who_bought/7.html;如果希望得到XML应答,可以请求/info/who_bought/7.xml。同样的方式可以扩展到任何内容类型(只要在respond_to代码块中编写了对应的处理程序即可)
要提供这样的行为,需要对路由配置稍作修改,打开config/routes.rb修改如下:
ActionController::Routing::Routes.draw do |map|
  map.connect ':controller/service.wsdl', :action => 'wsdl'
  map.connect ':controller/:action/:id'
  map.connect ':controller/:action/:id.:format'
end
新增的一行路由配置表示:请求的URL可能以文件扩展名(.html、.xml等)结束。遇到这样请求时,就会将扩展名保存到format变量,Rails则会根据这个变量来判断客户端首选的内容类型。
修改完成之后,重启应用程序,尝试请求:[url]http://localhost:3000/info/who_bought/7.xml[/url]随后会看到XML,或者空白,这跟浏览器有关。
自动生成XML
在前面例子中,手工编写了rxml模板,用它生成XML应答。这样可以控制返回的XML文档中各个元素的先后次序。但如果次序并不重要,也可以调用模型对象的to_xml()方法,让Rails根据模型对象自动生成XML文档。下面代码重新定义了针对XML请求的处理行为,用to_xml()方法生成XML应答:
info_controller.rb
  def who_bought
    @product = Product.find(params[:id])
    @orders  = @product.orders
    respond_to do |accepts|
      accepts.html
      accepts.xml{ render :xml=> @product.to_xml(:include=>:orders)}
    end
  end
:xml选项会告诉render()方法,把应答的内容类型设置为application/xml。随后,调用to_xml的结果就会被发送给客户端。此时,@products变量以及与之关联的Order对象都回被导出成XML。
C:\>curl [url]http://localhost:3000/info/who_bought/7.xml[/url]
<?xml version="1.0" encoding="UTF-8"?>
<product>
  <description>&lt;p&gt;
       &lt;em&gt;Pragmatic Project Automation&lt;/em&gt; shows you how to improv
e the
       consistency and repeatability of your project's procedures using
       automation to reduce risk and errors.
      &lt;/p&gt;
      &lt;p&gt;
        Simply put, we're going to put this thing called a computer to work
        for you doing the mundane (but important) project stuff. That means
        you'll have more time and energy to do the really
        exciting---and difficult---stuff, like writing quality code.
      &lt;/p&gt;</description>
  <id type="integer">7</id>
  <p_w_picpath-url>/p_w_picpaths/auto.jpg</p_w_picpath-url>
  <price type="decimal">29.95</price>
  <title>Pragmatic Project Automation</title>
  <orders>
    <order>
      <address>dp</address>
      <email>ilm</email>
      <id type="integer">1</id>
      <name>china</name>
      <pay-type>cc</pay-type>
    </order>
    <order>
      <address>china</address>
      <email>[email]ilmf@eyou.com[/email]</email>
      <id type="integer">2</id>
      <name>lee</name>
      <pay-type>cc</pay-type>
    </order>
    <order>
      <address>lfjs</address>
      <email>[email]fjd@china.com[/email]</email>
      <id type="integer">3</id>
      <name>chinaren</name>
      <pay-type>cc</pay-type>
    </order>
    <order>
      <address>dkf</address>
      <email>[email]zllichO@eyou.com[/email]</email>
      <id type="integer">4</id>
      <name>zllicho</name>
      <pay-type>check</pay-type>
    </order>
  </orders>
</product>
可以看到,默认情况下,to_xml方法会导出对象的所有信息。也可以要求它屏蔽某些属性,但这容易让代码变得凌乱。如果希望生成的XML符合特定的schema或者DTD,最后还是使用rxml模板。
扫尾工作
编程已经结束了,不过在将应用程序部署到生产环境之前,做一些整理工作。
为应用程序提供一份完整的文档。在Rails中可以运行Ruby提供的RDoc工具,为应用程序的所有源文件生成漂亮的开发者文档,不过在生成这些文档之前,可能需要创建一个介绍性的页面。这样未来开发者才会知道应用程序到底干了什么。为此编辑doc/README_FOR_APP文件,在里面写上有用的东西。稍后RDoc会对这个文件进行处理,所以可以随便在里面使用什么格式。
= The Depot Online Store

This application implements an online store, with a catalog, cart, and orders.

It is divided into three main sections:

* The buyer's side of the application manages the catalog, cart, and
  checkout. It is implemented in StoreController and the associated
  views.

* Only administrators can access stuff in the AdminController.
  This is implemented by the LoginController, and is enforced by the    
  ApplicationController#authorize method.

* There's also a simple web service accessible via the InfoController.

This code was produced as an example for the book {Agile Web
Development with
Rails}[[url]http://pragmaticprogrammer.com/titles/rails2[/url]]. It should not be
run as a real online store.

=== Author

Dave Thomas, The Pragmatic Programmers, LLC

=== Warranty

This code is provided for educational purposes only, and comes with
absolutely no warranty. It should not be used in live applications.

== Copyright

This code is Copyright (c) 2006 The Pragmatic Programmers, LLC.

It is released under the same license as Ruby.
然后,用rake命令就可以生成HTML格式的文档。(/doc/app)
depot>rake doc:app
最后,想看看自己编写了多少代码,有一个rake任务可以完成
depot>rake stats
MVC
Model负责维持应用程序的状态。不仅是数据的容器,还是数据的监护者。
View生成用户界面,通常会根据模型中的数据来生成。
Controller负责协调整个应用程序的运转。控制器接收来自外界的事件,与Model进行交互,并将合适的view展现给用户。