首页 > Web开发, 动态语言, 挨踢(IT) > 《Agile Web Development with Rails》抄书笔记(10):创建购物车

《Agile Web Development with Rails》抄书笔记(10):创建购物车

2013年5月24日 发表评论 阅读评论 511 人阅读    

《Agile Web Development with Rails》抄书笔记系列

  “《Agile Web Development with Rails》抄书笔记系列”目录

  上一章节,我们讲解了商品的展示。这一节,我们将向大家介绍购物车的创建。同时,这一节也将向大家展示如何在Rails中创建Session?如何建立Model直接的关联关系?

D呱呱

  关于这节内容的代码:

  1. 这节开始之前:https://github.com/diguage/depot/tree/v-08.4
  2. 这节完成之后:https://github.com/diguage/depot/tree/v-09.3

创建购物车

  有过上网购物经验的朋友都知道,当我们浏览一个商品列表时,会选择一些自己喜欢的东西,然后添加到虚拟的购物车中。购物车会保存用户的选择,选择完后,进一步地支付、购买所需的商品。

  这就意味着我们要保存用户上网的添加记录。为此,我们需要在数据库中保存一个购物车,同时将购物车的ID,cart.id,保存到Session中。每次访问Depot时,我们都要从Session中获取cart.id,然后根据这个cart.id从数据库中查找出相对应的购物车。

  首先,创建Cart,两条指令搞定:

rails generate scaffold cart
# 输出略

rake db:migrate
# 输出略

  同时,向%Depot%/app/controllers/application_controller.rb中添加如下代码,实现购物车的创建或查找:

class ApplicationController < ActionController::Base
  protect_from_forgery

  private
    def current_cart
      Cart.find(session[:cart_id])
    rescue ActiveRecord::RecordNotFound
      cart = Cart.create
       session[:cart_id] = cart.id
       cart
     end
end

  current_cart()方法会根据Session的cart_id从数据库中查找Cart;如果没有,则会创建一个,并且把Cart.id的值以cart_id为名称添加到Session中去。

建立Cart与Product的关联关系

  购物车Cart已经有了;产品Product也有了。Cart中可以有若干个指向Product的指针(暂且这样称呼);一个Product也可以被Cart所包含。根据关系型数据库的理论,可以很容易看出,为了保存这种关联关系,我们还需要创建一个中间表,当然这里也对应一个”关联对象”。两条相关指令如下:

rails generate scaffold line_item product_id:integer cart_id:integer
# 输出略

rake db:migrate
# 输出略

  现在,数据库层面已经建立起了关联关系;但是Rails层面的代码还没有关联起来,下面我们对其建立起关联关系。

  首先打开%Depot%/app/models/cart.rb文件,对其添加如下代码:

class Cart < ActiveRecord::Base
  # attr_accessible :title, :body
  has_many :line_items, dependent: :destroy
end

  has_many()方法的意义很明确:Cart中有多个关联的line items。

  dependent::destroy这段代码是说明,line items的生命周期依赖于Cart,如果Cart被消亡,则line items也跟随一起消亡。

  下面,我们建立起从line item到Cart,以及到Product的关联关系。打开文件%Depot%/app/models/line_item.rb,在里面添加如下代码:

class LineItem < ActiveRecord::Base
  attr_accessible :cart_id, :product_id
  belongs_to :product
  belongs_to :cart
end

  belongs_to()方法是向Rails说明,在line_items表中的数据,是关联carts表和products表的。另外,从这里也可以很容易看出,belongs_to()的用处,哪个表有外键,则就需要在相应的Model中添加belongs_to()方法。

  在Product中,我们也需要建立起对line_items的关联关系。另外,由于line_items表对products有外键关系。所以,删除products表中的记录时,要先检查一下line_items中是否有关联的记录,没有可以删除;有则不能删除。具体代码如下:

class Product < ActiveRecord::Base
  attr_accessible :description, :image_url, :price, :title
  validates :description, :image_url, :title, presence: true
  validates :price, numericality: {greater_than_or_equal_to: 0.01}
  validates :title, uniqueness: true
  validates :image_url, allow_blank: true, format: {
    with: %r{\.(gif|jpg|png)$}i,
    message: 'must be a URL for GIF, JPG or PNG image.'
  }

  has_many :line_items

  before_destroy :ensure_not_referenced_by_any_line_item

  private
   def ensure_not_referenced_by_any_line_item
     if line_items.empty?
       return true
     else
        errors.add(:base, 'Line Items present')
        return false
     end
   end
end

向购物车添加商品

  购物车的准备工作都已经搞好,下面我们实现向购物车Cart中添加商品以及展示的功能。

  大家也许已经注意到了,商品只展示信息,但是没有购买按钮,我们先把这个按钮加上。加上这个功能,我们就要有一个处理的Controller。需要再添加一个新的Controller吗?答案是否定的。大家可以看一下自动生成的那些Controller,一般都会包含index(), show(), new(), edit(), create(), update(), and destroy()等几个方法。create()比较贴合我们的需求。当然,也有些人认为new()看起来也可以,但是new()主要是用来和收集表单数据,添加新纪录用的。

  既然决定使用哪个方法了,那么我们创建什么呢?不是Product,也不是Cart,那么只有LineItem了。那么可以打开%Depot%/app/controllers/line_items_controller.rb文件,看一下create()方法的说明,可以看出,我们的这个选择决定了我们所用的URL为/line_items,使用的HTTP方法为POST。这些决定也间接地影响了我们的页面展示。在以前的章节中,我们使用link_to()方法来创建URL,但是链接所用的HTTP方法是GET,不符合我们的要求。我们想用POST,所以,这次我们添加一个Button,添加Button就要使用button_to()方法了。

  我们还需要将Button链接到一个特定的URL上。这点,Rails又可以帮我们一把,Rails”控制器名称”+”_path”的”法术”(借鉴《Ruby元编程》里面的术语)来生成URL。这里,我们使用line_items_path这个路径。

  除此之外,我们还需要向line_items_path()添加一个Product的id作为标识符,来让它知道我们想要添加哪个商品。这实现起来也很简单,只需要向line_items_path()方法添加可选参数:product_id,另外我们还要传Product的一个实例对象过去,Rails会从中提取出id方便使用。

  所需的业务和知识都已经说明完毕,下面我们开始修改代码。打开文件%Depot%/app/views/store/index.html.erb,在里面添加如下代码:

<h1>Your Pragmatic Catalog</h1>
<% @products.each do |product| %>
     <div class="entry">
          <%= image_tag(product.image_url) %>
          <h3><%= product.title %></h3>
          <%= sanitize(product.description) %>
          <div class="price_line">
               <span class="price"><%= number_to_currency(product.price) %></span>
               <%= button_to 'Add to Cart', line_items_path(product_id: product) %>
          </div>
     </div>
<% end %>

  这是,可以打开http://127.0.0.1:3000/,页面已经生成了按钮。然后查看一下网页源代码,你会发现这个button_to()方法实际上是生成了一个表单,而按钮就是表单中的提交按钮。这个按钮还被包含在div标签中,而默认的div是行显示。所以,要对显示效果做个调整,打开文件%Depot%/app/assets/stylesheets/store.css.scss,在里面添加如下代码:

    p, div.price_line {
      margin-left: 100px;
      margin-top: 0.5em;
      margin-bottom: 0.8em;
    }

    form, div{
      display: inline;
    }

  修改完了,记得使用rake assets:precompile编译一下CSS文件。

  页面修改完了,下面我们来修改一下后台Controller的代码。这段代码的业务是这样的:查找出当前Session的购物车,如果没有则创建一个,然后把选择的商品添加到购物车中,然后显示购物车中内容。%Depot%/app/controllers/line_items_controller.rb代码如下:

  def create
    @cart = current_cart
    product = Product.find(params[:product_id])
    @line_item = @cart.line_items.build(product: product)

    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

  current_cart()方法是上面在ApplicationController中实现的方法,用于查找构造创建当前的购物车;沟通params(:product_id)从HTTP请求中,取得名为product_id的参数值,然后使用Product.find(params[:product_id])从数据库中查找出该商品信息,创建Product对象。

D呱呱

  疑问:@cart.line_items.build(product: product)这段代码是什么意思?不是太明白。

功能性测试

  既然修改了Controller,我们来做一个功能测试。这时就要修改%Depot%/test/functional/line_items_controller_test.rb,我们需要测试的是调整。测试代码如下:

  test "should create line_item" do
    assert_difference('LineItem.count') do
      post :create, product_id: products(:ruby).id
    end

    assert_redirected_to cart_path(assigns(:line_item).cart)
  end

  然后,运行如下指令,开始测试:

rake test:functionals

  购物车展示

  经过测试,没有问题。那么,我们就修改购物车的展示。修改%Depot%/app/views/carts/show.html.erb如下:

<% if notice %>
<p id="notice"><%= notice %></p>
<% end %>

<h2>Your Pragmatic Cart</h2>

<ul>
  <% @cart.line_items.each do |item| %>
    <li><%= item.product.title %></li>
  <% end %>
</ul>

D呱呱

  在做这个练习的过程中,可能会遇到如下错误:

Can't mass-assign protected attributes: product

  这是由于Rails修改了安全规则。只需要在类LineItem的类体代码内,增加如下代码即可:

attr_accessible :product

  问题原因解释如下:

  上面的帖子估计看得很无趣,前两天天又看了一篇帖子,针对这个问题的渊源讲解的更透彻,也更有意思,贴出来,希望对遇到这个问题的朋友有所帮助:
  從 Github 被 Hack,談 Rails 的安全性( Mass-assignment )

  D瓜哥还在Ruby China上见了一个帖子,针对这个问题进行讨论,感兴趣的话请移步:遇到问题Can’t mass-assign protected attributes,麻烦帮忙解答一下

  OK,我们的购物车完成了。但是,运行一下你会发现,这个购物车的现实还有点问题。这个,我们将在下一节来优化。



作 者: D瓜哥,https://www.diguage.com/
原文链接:https://www.diguage.com/archives/16.html
版权声明:非特殊声明均为本站原创作品,转载时请注明作者和原文链接。

  1. 2013年5月28日22:10 | #1

    哈哈,非常感谢你的这个系列文章 让我对 Ruby on Rails 感兴趣。!
    从上周5 看了你的这个系列,禁不住自己动手尝试,这两三天以来还是遇到了一些问题,通过 Google 基本都解决了。
    但是作为一个前端开发,还是蛮多不理解的,自己动手也仅仅只是照葫芦画瓢,该系列文档中也有很多是没有说清楚的,同时也很期待你接下来的笔记 :)

    刚才了解 关系型数据库的时候,发现了这篇文章: http://guides.ruby-china.org/getting_started.html ,里边对很多说明还算详细,分享之,希望能对你,或者其他感兴趣的朋友有所帮助。

    谢谢。

    • D瓜哥
      2013年5月30日22:30 | #2

      对你有所帮助就行。下面的文章已经写出来一部分了,还没发。抽空发。不过,还没完,还得接着写。哈哈

  2. 2013年6月5日00:03 | #3

    辛苦了, 赞一个

  3. 2013年6月8日21:30 | #4

    哈哈,我也在学 Rails,觉得看书十遍,不如动手一遍

  1. 2013年6月12日23:12 | #1
  2. 2013年6月13日23:15 | #2
  3. 2013年6月18日22:54 | #3