《Agile Web Development with Rails》抄书笔记系列
“《Agile Web Development with Rails》抄书笔记系列”目录
上一章节,我们讲解了商品的展示。这一节,我们将向大家介绍购物车的创建。同时,这一节也将向大家展示如何在Rails中创建Session?如何建立Model直接的关联关系?
D呱呱
关于这节内容的代码:
- 这节开始之前:https://github.com/diguage/depot/tree/v-08.4
- 这节完成之后:https://github.com/diguage/depot/tree/v-09.3
创建购物车
有过上网购物经验的朋友都知道,当我们浏览一个商品列表时,会选择一些自己喜欢的东西,然后添加到虚拟的购物车中。购物车会保存用户的选择,选择完后,进一步地支付、购买所需的商品。
这就意味着我们要保存用户上网的添加记录。为此,我们需要在数据库中保存一个购物车,同时将购物车的ID,cart.id,保存到Session中。每次访问Depot时,我们都要从Session中获取cart.id,然后根据这个cart.id从数据库中查找出相对应的购物车。
首先,创建Cart,两条指令搞定:
1 | rails generate scaffold cart |
同时,向%Depot%/app/controllers/application_controller.rb中添加如下代码,实现购物车的创建或查找:
01 | class ApplicationController < ActionController::Base |
06 | Cart.find(session[ :cart_id ]) |
07 | rescue ActiveRecord::RecordNotFound |
09 | session[ :cart_id ] = cart.id |
current_cart()方法会根据Session的cart_id从数据库中查找Cart;如果没有,则会创建一个,并且把Cart.id的值以cart_id为名称添加到Session中去。
建立Cart与Product的关联关系
购物车Cart已经有了;产品Product也有了。Cart中可以有若干个指向Product的指针(暂且这样称呼);一个Product也可以被Cart所包含。根据关系型数据库的理论,可以很容易看出,为了保存这种关联关系,我们还需要创建一个中间表,当然这里也对应一个”关联对象”。两条相关指令如下:
1 | rails generate scaffold line_item product_id :integer cart_id :integer |
现在,数据库层面已经建立起了关联关系;但是Rails层面的代码还没有关联起来,下面我们对其建立起关联关系。
首先打开%Depot%/app/models/cart.rb文件,对其添加如下代码:
1 | class Cart < ActiveRecord::Base |
3 | has_many :line_items , dependent: :destroy |
has_many()方法的意义很明确:Cart中有多个关联的line items。
dependent::destroy这段代码是说明,line items的生命周期依赖于Cart,如果Cart被消亡,则line items也跟随一起消亡。
下面,我们建立起从line item到Cart,以及到Product的关联关系。打开文件%Depot%/app/models/line_item.rb,在里面添加如下代码:
1 | class LineItem < ActiveRecord::Base |
2 | attr_accessible :cart_id , :product_id |
belongs_to()方法是向Rails说明,在line_items表中的数据,是关联carts表和products表的。另外,从这里也可以很容易看出,belongs_to()的用处,哪个表有外键,则就需要在相应的Model中添加belongs_to()方法。
在Product中,我们也需要建立起对line_items的关联关系。另外,由于line_items表对products有外键关系。所以,删除products表中的记录时,要先检查一下line_items中是否有关联的记录,没有可以删除;有则不能删除。具体代码如下:
01 | class Product < ActiveRecord::Base |
02 | attr_accessible :description , :image_url , :price , :title |
03 | validates :description , :image_url , :title , presence: true |
04 | validates :price , numericality: {greater_than_or_equal_to: 0 . 01 } |
05 | validates :title , uniqueness: true |
06 | validates :image_url , allow_blank: true , format: { |
07 | with: %r{\.(gif|jpg|png)$}i, |
08 | message: 'must be a URL for GIF, JPG or PNG image.' |
13 | before_destroy :ensure_not_referenced_by_any_line_item |
16 | def ensure_not_referenced_by_any_line_item |
20 | errors.add( :base , 'Line Items present' ) |
向购物车添加商品
购物车的准备工作都已经搞好,下面我们实现向购物车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,在里面添加如下代码:
01 | < h1 >Your Pragmatic Catalog</ h1 > |
02 | <% @products.each do |product| %> |
04 | <%= image_tag(product.image_url) %> |
05 | < h3 ><%= product.title %></ h3 > |
06 | <%= sanitize(product.description) %> |
07 | < div class = "price_line" > |
08 | < span class = "price" ><%= number_to_currency(product.price) %></ span > |
09 | <%= button_to 'Add to Cart', line_items_path(product_id: product) %> |
这是,可以打开http://127.0.0.1:3000/,页面已经生成了按钮。然后查看一下网页源代码,你会发现这个button_to()方法实际上是生成了一个表单,而按钮就是表单中的提交按钮。这个按钮还被包含在div标签中,而默认的div是行显示。所以,要对显示效果做个调整,打开文件%Depot%/app/assets/stylesheets/store.css.scss,在里面添加如下代码:
修改完了,记得使用rake assets:precompile编译一下CSS文件。
页面修改完了,下面我们来修改一下后台Controller的代码。这段代码的业务是这样的:查找出当前Session的购物车,如果没有则创建一个,然后把选择的商品添加到购物车中,然后显示购物车中内容。%Depot%/app/controllers/line_items_controller.rb代码如下:
03 | product = Product.find(params[ :product_id ]) |
04 | @line_item = @cart .line_items.build(product: product) |
06 | respond_to do |format| |
08 | format.html { redirect_to @line_item .cart, |
09 | notice: 'Line item was successfully created.' } |
10 | format.json { render json: @line_item , status: :created , location: @line_item } |
12 | format.html { render action: "new" } |
13 | format.json { render json: @line_item .errors, status: :unprocessable_entity } |
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,我们需要测试的是调整。测试代码如下:
1 | test "should create line_item" do |
2 | assert_difference( 'LineItem.count' ) do |
3 | post :create , product_id: products( :ruby ).id |
6 | assert_redirected_to cart_path(assigns( :line_item ).cart) |
然后,运行如下指令,开始测试:
购物车展示
经过测试,没有问题。那么,我们就修改购物车的展示。修改%Depot%/app/views/carts/show.html.erb如下:
02 | < p id = "notice" ><%= notice %></ p > |
05 | < h2 >Your Pragmatic Cart</ h2 > |
08 | <% @cart.line_items.each do |item| %> |
09 | < li ><%= item.product.title %></ li > |
D呱呱
在做这个练习的过程中,可能会遇到如下错误:
1 | Can't mass-assign protected attributes: product |
这是由于Rails修改了安全规则。只需要在类LineItem的类体代码内,增加如下代码即可:
1 | attr_accessible :product |
问题原因解释如下:
上面的帖子估计看得很无趣,前两天天又看了一篇帖子,针对这个问题的渊源讲解的更透彻,也更有意思,贴出来,希望对遇到这个问题的朋友有所帮助:
從 Github 被 Hack,談 Rails 的安全性( Mass-assignment )
D瓜哥还在Ruby China上见了一个帖子,针对这个问题进行讨论,感兴趣的话请移步:遇到问题Can’t mass-assign protected attributes,麻烦帮忙解答一下
OK,我们的购物车完成了。但是,运行一下你会发现,这个购物车的现实还有点问题。这个,我们将在下一节来优化。
哈哈,非常感谢你的这个系列文章 让我对 Ruby on Rails 感兴趣。!
从上周5 看了你的这个系列,禁不住自己动手尝试,这两三天以来还是遇到了一些问题,通过 Google 基本都解决了。
但是作为一个前端开发,还是蛮多不理解的,自己动手也仅仅只是照葫芦画瓢,该系列文档中也有很多是没有说清楚的,同时也很期待你接下来的笔记
刚才了解 关系型数据库的时候,发现了这篇文章: http://guides.ruby-china.org/getting_started.html ,里边对很多说明还算详细,分享之,希望能对你,或者其他感兴趣的朋友有所帮助。
谢谢。
对你有所帮助就行。下面的文章已经写出来一部分了,还没发。抽空发。不过,还没完,还得接着写。哈哈
辛苦了, 赞一个
哈哈,我也在学 Rails,觉得看书十遍,不如动手一遍