Ruby On Rails的第一个应用(七)--更智能的购物车

V 任务E:更智能的购物车

 

·个性数据库模式(schema)与现有数据

·诊断和处理错误

·闪存

·日志

 

一、迭代E1:创建更智能的购物车

 

1.由于购物车中每个产品都有一个关联的计数器,这就要求个性line_items表。

Administrator@JARRY /e/works/ruby/depot (master)
$ rails generate migration add_quantity_to_line_items quantity:integer
      invoke  active_record
      create    db/migrate/20130326064705_add_quantity_to_line_items.rb

  

rails有两种匹配模式:add_XXX_to_TABLE和remove_XXX_to_TABLE,这里XXX被忽略,的是出现在迁移名之后的字段及其类型的清单。

rails无法判断字段的默认值,一般默认值为null,但是我们把已有的购物车的默认值设置为1。在应用迁移前,要先修改/db/migrate/20130326064705_add_quantity_to_line_items.rb

class AddQuantityToLineItems < ActiveRecord::Migration
  def self.up
    add_column :line_items, :quantity, :integer, default: 1
  end
  def self.down
    remove_column :line_items, :quantity
  end
end
 

  

2.执行迁移

Administrator@JARRY /e/works/ruby/depot (master)
$ rake db:migrate
==  AddQuantityToLineItems: migrating =========================================
-- add_column(:line_items, :quantity, :integer, {:default=>1})
   -> 0.0469s
==  AddQuantityToLineItems: migrated (0.0625s) ================================
 

 

3.现在Cart中需要一个聪明的add_product方法,该方法用来判断商品清单中是否已包含了想要添加的产品:如果是的话,那就增加数量;如果不是的话,就生成一个新的LineItem。修改/app/models/cart.rb

class Cart < ActiveRecord::Base
  has_many :line_items, dependent: :destroy
  
  def add_product(product_id)
    current_item = line_items.find_by_product_id(product_id)
    if current_item
      current_item.quantity += 1
    else
      current_item = line_items.build(product_id: product_id)
    end
    current_item
  end 
end
 

 

这里调用了find_by_product_id方法,但是没有定义过。Active Record模块注意到调用未定义的方法,并且发现在其名称是以字符串find_by开始和字段名结束,于是ActiveRecord模块动态地构造了查询器方法,并将其添加到类中。

 

4.为了使用add_product方法,还要修改商品项目控制器的create方法

/app/controllers/line_items_controller.rb

  def create
    @cart = current_cart
    product = Product.find(params[:product_id])
    @line_item = @cart.line_items.add_product(product.id)
 
    respond_to do |format|
      if @line_item.save
        format.html { redirect_to @line_item.cart, notice: 'Line item was successfully created.' }
        format.json { render json: @line_item, status: :created, location: @line_item }
      else
        format.html { render action: "new" }
        format.json { render json: @line_item.errors, status: :unprocessable_entity }
      end
    end
  end
 

 

5.为了使用新信息,最后还需要修改show视图

/app/views/carts/show.html.erb

<h2>Your Pragmatic Cart<h2>
<ul>
<% @cart.line_items.each do |item| %>
<li><%= item.quantity %> &times; <%= item.product.title %></li>
<% end %>
</ul>
 

 

6.再次点击add to cart 添加已买过的商品,如图:

 Ruby On Rails的第一个应用(七)--更智能的购物车_第1张图片

7.

Administrator@JARRY /e/works/ruby/depot (master)
$ rails generate migration combine_items_in_cart
 
      invoke  active_record
      create    db/migrate/20130326072305_combine_items_in_cart.rb
 

 

8.现在rails完全推断不出想做什么了,所以,这次完全由我们来填写self.up方法/db/migrate/20130326072305_combine_items_in_cart.rb:

  def self.up
    # replace multiple items for a single product in a cart with a single item
    Cart.all.each do |cart|
      # count the number of each product in the cart
      sums = cart.line_items.group(:product_id).sum(:quantity)
      
      sums.each do |product_id, quantity|
        if quantity > 1
          # remove individual items
          cart.line_items.where(product_id: product_id).delete_all
          
          # replace with a single item
          cart.line_items.create(product_id: product_id, quantity: quantity)
        end
      end
    end
  end

 

先从迭代每个购物车开始;对于每个购物车及其每个相关联的商品项目,按照字段product_id进行编组,得出各字段数量之和,计算结果将是字段product_ids和数量对的有序列表;然后迭代每一组之和,从每一个组中提取product和quantity;对于数量大于1的组,将删除与该购物车和该产品相关联的所有单个的商品项目,然后用正确数量的单行商品来替代它们。

 

9.应用迁移

Administrator@JARRY /e/works/ruby/depot (master)
$ rake db:migrate
==  CombineItemsInCart: migrating =============================================
==  CombineItemsInCart: migrated (0.4531s) ====================================
 

 

10.查看购物车查看结果

 Ruby On Rails的第一个应用(七)--更智能的购物车_第2张图片

11.迁移的一个重要原则是每一步都是可逆的,所以,还要实现了一个self.down方法。这种方法用于查找数量大于1的商品项目:为该购物车和产品添加一个新的商品项目,一个数量增加一行,最后删除该商品项目多余的行。该操作的代码如下/db/migrate/20130326072305_combine_items_in_cart.rb:

  def self.down
    # split items with quantity>1 into multiple items
    LineItem.where("quantity>1").each do |line_item|
      # add individual items
      line_item.quantity.times do
        LineItem.create cart_id:line_item.cart_id, product_id: line_item.product_id, quantity: 1
      end
      
      # remove original item
      line_item.destroy
    end
  end
 

 

11.回滚迁移,并查看购物车来验证结果。

Administrator@JARRY /e/works/ruby/depot (master)
$ rake db:rollback
==  CombineItemsInCart: reverting =============================================
==  CombineItemsInCart: reverted (0.2812s) ====================================

Ruby On Rails的第一个应用(七)--更智能的购物车_第3张图片

 

12.重新应用迁移使用命令rake db:migrate

 

二、迭代E2:错误处理

有种攻击:通过传递带错误参数的请求到web应用程序。购物车的链接看起来像carts/nnn,其中nnn是内部的购物车id。感觉这个不是很好,直接在浏览器上输入这个请求,并传个字符串wibble。应用程序将出现如下错误信息:

Ruby On Rails的第一个应用(七)--更智能的购物车_第4张图片

这里暴露了太多的应用程序的信息,看上去很不专业,因此我们要使应用程序有更强的韧性。

1.上图中可以看到:

app/controllers/carts_controller.rb:16:in `show'

这里抛出了异常,即这行:

    @cart = Cart.find(params[:id])

如果无法找到购物车,ActiveRecord模块会抛出一个RecordNotFound的异常,显然我们需要处理这个异常。

Rails提供了方便的处理错误和报告错误的方法。它定义了称为闪存(flash)的结构。闪存是一个桶(bucket,实际上更像个散列),当处理请求时,可以在其中存储东西。对于同一会话的下次请求,在自动地删除闪存内容之前,闪存中的内容都是有效的。

通常情况下闪存是用来收集错误信息的。在视图中可以用flash存取器方法(accessor method)来访问闪存的信息。

闪存数据存储在会话中,以使其能在请求与请求的中间被访问。

现在修改show方法来拦截无效的产品id并报告问题:

/app/controllers/carts_controller.rb

 

  def show
    begin
      @cart = Cart.find(params[:id])
    rescue ActiveRecord::RecordNotFound
      logger.error "Attempt to acces invalid cart #{params[:id]}"
      redirect_to store_url, notice: 'Invalid cart'
    else
      respond_to do |format|
        format.html # show.html.erb
        format.json { render json: @cart }
      end
    end
  end
 

  

2.刷新http://localhost:3000/carts/wibble,没有出现错误信息了,显示了目录网页。如图:

 Ruby On Rails的第一个应用(七)--更智能的购物车_第5张图片

另外从/log/development.log可以找到Attempt to acces invalid cart wibble日志信息。

 

三、迭代E3:对购物车的最后加工

现在还有个问题,没有办法清空购物车。

清空购物车要在购物车中添加个链接和修改购物车控制器中的destroy方法来清理会话。

1.先从模板开始,并再次用button_to方法给页面添加个按钮:

/app/views/carts/show.html.erb

<h2>Your Pragmatic Cart<h2>
<ul>
<% @cart.line_items.each do |item| %>
<li><%= item.quantity %> &times; <%= item.product.title %></li>
<% end %>
</ul>
 
<%= button_to 'Empty Cart', @cart, method: :delete, confirm: 'Are you sure?' %>

 

2.在控制器中修改destory方法,以确保用户只是删除他自己的购物车,并在重定向到索引页面之前(带有通知消息),从会话中删除该购物车:

/app/controllers/carts_controller.rb

def destroy
    @cart = current_cart
    @cart.destroy
    session[:cart_id] = nil
 
    respond_to do |format|
      format.html { redirect_to store_url, notice: 'Yout cart is currently empty!' }
      format.json { head :ok }
    end
  end
  

 

3.然后更新对应的测试/test/functional/carts_controller_test.rb:

  test "should destroy cart" do
    assert_difference('Cart.count', -1) do
      session[:cart_id] = @cart.id
      delete :destroy, id: @cart.to_param
    end
 
    assert_redirected_to store_path
  end

 

4.点击页面Empty Cart按钮,查看效果:

Ruby On Rails的第一个应用(七)--更智能的购物车_第6张图片

 Ruby On Rails的第一个应用(七)--更智能的购物车_第7张图片

5.添加新的商品项目时,也可以删除那个自动生成的闪存消息:

/app/controllers/line_items_controller.rb

  def create
    @cart = current_cart
    product = Product.find(params[:product_id])
    @line_item = @cart.add_product(product.id)
 
    respond_to do |format|
      if @line_item.save
        format.html { redirect_to @line_item.cart } # here! remove the notice.
        format.json { render json: @line_item, status: :created, location: @line_item }
      else
        format.html { render action: "new" }
        format.json { render json: @line_item.errors, status: :unprocessable_entity }
      end
    end
  end
 

 

6.用表格来整理下购物车页面,并用css制作样式:

/app/views/carts/show.html.erb

<div class="cart_title">Your Cart</div>
<table>
<% @cart.line_items.each do |item| %>
<tr>
<td><%= item.quantity %> &times;</td>
<td><%= item.product.title %></td>
<td class="item_price"><%= number_to_currency(item.total_price, unit: "¥") %></td>
</tr>
<% end %>
<tr class="total_line">
<td colspan="2">Total</td>
<td class="total_cell"><%= number_to_currency(@cart.total_price, unit: "¥")%></td>
</tr>
</table>
 
<%= button_to 'Empty Cart', @cart, method: :delete, confirm: 'Are you sure?' %>

 

7.要让这个代码运行起来,要分别在LineItem和Cart模型中添加方法计算总价。

/app/models/line_item.rb:

  def total_price
    product.price * quantity
  end 

 

/app/models/cart.rb:

  def total_price
    line_items.to_a.sum{|item| item.total_price }
  end

   

然后再在修改/app/assets/stylesheets/store.css.scss,在.store{}里面添加:

/* Styles for the cart in the main page */
 
.cart_title{
  font: 120%;
  font-weight: bold;
}
 
.item_price, .total_line{
  text-align: right;
  padding: 0 0 0 1em;
}
 
.total_line .total_cell{
  font-weight: bold;
  border-top: 1px solid #595;
}
总价计算显示效果:
Ruby On Rails的第一个应用(七)--更智能的购物车_第8张图片


 

你可能感兴趣的:(on,on,Ruby,Ruby,Rails,Rails,depot,Rails的第一个应用)