AWDR 阅读笔记 – 6.Code – Task E

Agile Web Development with Rails 5.1 阅读笔记系列第 6 篇,继续上一篇对在线书店进行迭代,对购物车进行一定改进,使其能检测重复项目、进行清空操作,参考本书该部分(CH10 – Task E)

Creating a Smarter Cart

Quantity Migration

建立 migration,向 line_items 表中添加字段 quantity。

$ bin/rails generate migration add_quantity_to_line_items quantity:integer

同时,quantity 的默认值置 1。

db/migrate/20181202142604_add_quantity_to_line_items.rb

class AddQuantityToLineItems < ActiveRecord::Migration[5.2]
  def change
    add_column :line_items, :quantity, :integer, default: 1
  end
end

执行:

$ bin/rails db:migrate

Add Methods

add_product

现在,添加项目的时首先被调用的方法为 add_product。此方法负责对数量进行检测。

app/models/cart.rb

class Cart < ApplicationRecord
  # ...

  def add_product product
    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

该方法的使用位置在于 line_item_controller#create

def create
  product = Product.find(params[:product_id])
  @line_item = @cart.add_product(product)
  # ...
end

此时为了测试效果,可以在 view/carts/show 中显示 quantity 字段。

<li><%= item.quantity %> × <%= item.product.title %></li>

Combine-Items Migration

为使旧购物车项目数据能聚合起来显示,即合并相同项,同时更新 quantity,需要一个新的 Migration。该 Migration 既可以合并相同项,也可以拆分相同项。

$ bin/rails generate migration combine_items_in_cart

Up

combine_item_in_cart 中的 up 方法用于合并相同项。

class CombineItemsInCart < ActiveRecord::Migration[5.2]
  def 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
          item = cart.line_items.build(product_id: product_id)
          item.quantity = quantity
          item.save!
        end
      end
    end
  end

  # ...
end

当我们再次执行 db:migrate 时,相同项就能合并。

$ bin/rails db:migrate

Down

反之,down 即 up 的逆操作。

class CombineItemsInCart < ActiveRecord::Migration[5.2]
  # ...

  def 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 orignal item
      line_item.destroy
    end
  end
end

反之即可回滚。

$ bin/rails db:rollback

可通过以下命令查看所有 migrations。

$ bin/rails db:migrate:status

Test

这一部分的测试只需要考虑 line_item_controller,测试在 line_item/show.erb 的 quantity 后面添加的 &time。

test/controllers/line_items_controller_test.rb

test "should create line_item" do
  # ...

  assert_select 'h2', 'Your Pragmatic Cart'
  assert_select 'li', '1 u00D7 Programming Ruby 1.9'
end

Handling Errors

如果在 url 中输入不存在的 cart_id,这个系统是没有能力抵御这种恶意试探的。

The invalid_cart Method

向 cart_controller 中添加用于处理错误 cart_id 的方法 invalid_cart。当 id 不存在时,该方法将会生成错误 flash,然后跳转。

class CartsController < ApplicationController
  # ...
  rescue_from ActiveRecord::RecordNotFound, with: :invalid_cart
  # ...
  private
    # ...

    def invalid_cart
      logger.error "Attempt to access invalid cart #{params[:id]}"
      redirect_to store_index_url, notice: 'Invalid cart'
    end
end

invalid_cart 中,也启用了 logger,将会把错误的 id 记录到日志中。

The cart_id Parems

在 line_items_controller 中,取消允许 :cart_id 参数。

class LineItemsController < ApplicationController
  # ...
  private
    # ...
    # Never trust parameters from the scary internet, only allow the white list through.
    def line_item_params
      # params.require(:line_item).permit(:product_id, :cart_id)
      params.require(:line_item).permit(:product_id)
    end
end

同时更新对应测试模块。

test "should update line_item" do
  patch line_item_url(@line_item), params: { line_item: { product_id: @line_item.product_id } }
  assert_redirected_to line_item_url(@line_item)
end

然后检查测试是否正确。

$ bin/rails log:clear LOGS=test
$ bin/rails test:controllers

Finishing the Cart

最后一部分,完成清空购物车功能,以及前端的迭代。

Empty Cart

重新定义 carts_controller 中的 destroy 方法。

class CartsController < ApplicationController
  # ...

  # DELETE /carts/1
  # DELETE /carts/1.json
  def destroy
    @cart.destroy if @cart.id == session[:cart_id]
    session[:cart_id] = nil
    respond_to do |format|
      format.html { redirect_to store_index_url, notice: 'Your cart is currently empty' }
      format.json { head :no_content }
    end
  end
  # ...
end

更新对应测试模块。

test "should destroy cart" do
  post line_items_url, params: { product_id: products(:ruby).id }
  @cart = Cart.find(session[:cart_id])

  assert_difference('Cart.count', -1) do
    delete cart_url(@cart)
  end

  assert_redirected_to store_index_url
end

前端部分换用表格显示,并添加 delete 按钮。

app/views/carts/show.html.erb

<article>
  <% if notice %>
    <aside id="notice"><%= notice %></aside>
  <% end %>
  
  <h2>Your Cart</h2>
  <table>
    <% @cart.line_items.each do |line_item| %>
      <tr>
        <td class="quantity"><%= line_item.quantity %></td>
        <td><%= line_item.product.title %></td>
        <td class="price"><%= number_to_currency(line_item.total_price) %></td>
      </tr>
    <% end %>

    <tfoot>
      <tr>
        <th colspan="2">Total:</th>
        <td class="price"><%= number_to_currency(@cart.total_price) %></td>
    </tfoot>
  </table>
  
  <%= button_to 'Empty cart', @cart,
                method: :delete,
                data: { confirm: 'Are you sure?' } %>
</article>  

为了能计算总价格,需要添加 line_item#total_price 和 cart#total_price。

app/models/line_item.rb

class LineItem < ApplicationRecord
  # ...

  def total_price
    product.price * quantity
  end
end

app/models/cart.rb

class Cart < ApplicationRecord
  # ...

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

对应 scss。

collapse 表格边框和卡其色清空按钮。

.carts {
  table {
    border-collapse: collapse;

    td {
      padding: .5em;

      &.quantity {
        white-space: nowrap;

        &::after {
          content: ' ×';
        }
      }

      &.price {
        font-weight: bold;
        text-align: right;
      }
    }

    tfoot {
      th, td.price {
        font-weight: bold;
        padding-top: 1em;
      }

      th {
        text-align: right;
      }

      td.price {
        border-top: solid thin;
      }
    }
  }

  input[type='submit'] {
    background-color: #881;
    border-radius: .354em;
    border: solid thin #441;
    color: white;
    font-size: 1em;
    padding: .354em 1em;
  
    &:hover {
      cursor: pointer;
      background-color: #992;
    }
  }
}

更新 line_items_controller 对应测试模块。

test "should create line_item" do
  assert_difference('LineItem.count') do
    post line_items_url, params: { product_id: products(:ruby).id }
  end
  follow_redirect!

  assert_select 'h2', 'Your Cart'
  assert_select 'li', 'Programming Ruby 1.9'
end

参考:

  1. Agile Web Development with Rails 5.1 . Sam Ruby. David Bryant Copeland. with Dave Thomas. 2017 The Pragmatic Programmers, LLC.
  2. 完整源码见:https://media.pragprog.com/titles/rails51/code/rails51/depot_g/*
  3. https://guides.rubyonrails.org/debugging_rails_applications.html#the-logger
  4. https://www.fileformat.info/info/unicode/char/00d7/index.htm

作者: V

Web Dev

發表迴響

在下方填入你的資料或按右方圖示以社群網站登入:

WordPress.com 標誌

您的留言將使用 WordPress.com 帳號。 登出 /  變更 )

Google photo

您的留言將使用 Google 帳號。 登出 /  變更 )

Twitter picture

您的留言將使用 Twitter 帳號。 登出 /  變更 )

Facebook照片

您的留言將使用 Facebook 帳號。 登出 /  變更 )

連結到 %s