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
Your Pragmatic Cart
<% @cart.line_items.each do |item| %>
- <%= item.quantity %> × <%= item.product.title %>
<% end %>
6.再次点击add to cart 添加已买过的商品,如图:
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.查看购物车查看结果
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) ====================================
12.重新应用迁移使用命令rake db:migrate
二、迭代E2:错误处理
有种攻击:通过传递带错误参数的请求到web应用程序。购物车的链接看起来像carts/nnn,其中nnn是内部的购物车id。感觉这个不是很好,直接在浏览器上输入这个请求,并传个字符串wibble。应用程序将出现如下错误信息:
这里暴露了太多的应用程序的信息,看上去很不专业,因此我们要使应用程序有更强的韧性。
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,没有出现错误信息了,显示了目录网页。如图:
另外从/log/development.log可以找到Attempt to acces invalid cart wibble日志信息。
三、迭代E3:对购物车的最后加工
现在还有个问题,没有办法清空购物车。
清空购物车要在购物车中添加个链接和修改购物车控制器中的destroy方法来清理会话。
1.先从模板开始,并再次用button_to方法给页面添加个按钮:
/app/views/carts/show.html.erb
Your Pragmatic Cart
<% @cart.line_items.each do |item| %>
<%= button_to 'Empty Cart', @cart, method: :delete, confirm: 'Are you sure?' %>- <%= item.quantity %> × <%= item.product.title %>
<% end %>
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按钮,查看效果:
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
Your Cart
<%= item.quantity %> × | <%= item.product.title %> | <%= number_to_currency(item.total_price, unit: "¥") %> |
Total | <%= number_to_currency(@cart.total_price, unit: "¥")%> |
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; }