Daily Archives: 30/11/2016

Rails – Xây dựng chức năng đặt hàng

Trong phần này chúng ta sẽ xây dựng chức năng đặt hàng đơn giản.

Chỉnh sửa model

Đầu tiên chúng ta định nghĩa một model có tên order (đơn hàng) bao gồm các trường:

  • name: tên người nhận
  • address: địa chỉ giao hàng
  • email: địa chỉ email
  • pay_type: phương thức thanh toán
C:\Projects\Rails\depot>rails generate scaffold order name:string address:text email:string pay_type:string
...

Khi một khách hàng thêm sản phẩm vào giỏ hàng thì một đối tượng LineItem sẽ được tạo ra, và chúng ta sẽ cần biết đối tượng LineItem này nằm trong giỏ hàng nào, do đó chúng ta sẽ thêm trường order_id vào model order như sau:

C:\Projects\Rails\depot>rails generate migration add_order_id_to_line_item order_id:integer
...

Cuối cùng chúng ta cập nhật lại cấu trúc CSDL:

C:\Project\Rails\depot>rake db:migrate
...

Chỉnh sửa View

Chúng ta thêm nút ‘Checkout’ vào giao diện như sau:

<h2>Your cart</h2>
<table>
    <%= render cart.line_items %>
    <tr class="total_line">
        <td colspan="2">Total</td>
        <td class="total_cell"><%= number_to_currency(cart.total_price) %></td>
    </tr>
</table>
 
<%= button_to 'Checkout', 
              new_order_path, 
              :method => :get %><br>
<%= button_to 'Empty cart', 
              cart, 
              :method => :delete,
              data: {:confirm => 'Are you sure?' } %>

Nút này sẽ gửi lên URL là '/orders/new' có trong biến new_order_path, phương thức gửi là GET, chúng ta phải chỉ rõ phương thức gửi ra vì mặc định hàm button_to gửi theo phương thức POST.

URL '/orders/new' được gọi tới phương thức action là new trong lớp OrdersController, phương thức này sẽ làm nhiệm vụ hiển thị form nhập thông tin đơn hàng, tuy nhiên chúng ta sửa lại để kiểm tra xem giỏ hàng hiện tại thật sự có hàng hay không thì mới hiển thị. Chúng ta sửa file orders_controller.rb trong thư mục app/controllers như sau:

class OrdersController < ApplicationController
    .
    .
    .
    # GET /orders/new
    def new 
        @cart = current_cart
        if @cart.line_items.empty?
            redirect_to '/', :notice => 'Your cart is empty'
            return 
        end
 
        @order = Order.new 
    end
    .
    .
    .
end

Nếu không có hàng trong giỏ hàng thì chúng ta cho quay lại trang '/', có tin nhắn thông báo là 'Your cart is empty'.

Ngược lại thì file View mặc định do Rails tạo ra là new.html.erb trong thư mục app/views/orders sẽ được hiển thị, chúng ta sửa lại như sau:

<div class="depot_form">
    <fieldset>
        <legend>Your information</legend>
        <%= render 'form' %> 
    </fieldset>
</div>

Ở đây chúng ta gọi hàm render với tham số là ‘form’, hàm này sẽ hiển thị form nhập thông tin từ file partial là _form.html.erb trong thư mục app/views/orders. File này Rails cũng tự động tạo cho chúng ta rồi, nhưng chúng ta sửa lại như sau:

<%= form_for(@order) do |f| %>
    <% if @order.errors.any? %>
        <div id="error_explanation">
            <h2>
                <%= pluralize(@order.errors.count, "error") %> prohibited this order from being saved:
            </h2>

            <ul>
                <% @order.errors.full_messages.each do |message| %>
                    <li><%= message %></li>
                <% end %>
            </ul>
        </div>
    <% end %>

    <div class="field">
        <%= f.label :name %><br>
        <%= f.text_field :name, :size => 40 %>
    </div>
    <div class="field">
        <%= f.label :address %><br>
        <%= f.text_area :address, :rows => 3, :cols => 40 %>
    </div>
    <div class="field">
        <%= f.label :email %><br>
        <%= f.text_field :email, :size => 40 %>
    </div>
    <div class="field">
        <%= f.label :pay_type %><br>
        <%= f.select :pay_type, Order::PAYMENT_TYPES, :prompt => 'Select a payment method' %>
    </div>
 
    <div class="actions">
        <%= f.submit 'Place order' %>
    </div>
<% end %>

Hàm form_for là một hàm helper giúp tạo form một cách nhanh chóng. Cú pháp có dạng:

<%= form_for <đối tượng> ... do |f| %>
    <%= f.label ... %>
    <%= f.text_area ... %>
<% end %>

Hàm form_for nhận vào một đối tượng, và tạo một đối tượng thuộc lớp ActionView::Helpers::FormaBuilder, ở đây là 'f', chúng ta có thể gọi các phương thức của đối tượng này để tạo các thẻ trong <form>, chẳng hạn như label, select, text_area, submit…v.v bạn có thể xem danh sách tại đây.

Ở phương thức f.select dùng để tạo combobox, chúng ta cho danh sách các phần tử là hằng số PAYMENT_TYPES trong lớp Order (app/models/order.rb), do đó chúng ta định nghĩa thêm hằng số này như sau:

class Order < ActiveRecord::Base
    PAYMENT_TYPES = [ "Cash on delivery", "Ngân lượng", "Bảo Kim", "Bank Card" ]
end

Cuối cùng chúng ta định nghĩa thêm một số lớp CSS như sau:

...
/* Order form style */
.depot_form fieldset {
    background: #efe;
}

.depot_form legend {
    color: #dfd;
    background: #141;
    font-family: sans-serif;
    padding: 0.2em 1em;
}

.depot_form label {
    width: 5em;
    float: left;
    text-align: right;
    padding-top: 0.2em;
    margin-right: 0.1em;
    display: block;
}

.depot_form select, .depot_form textarea, .depot_forma input {
    margin-left: 0.5em;
}

.depot_form .submit {
    margin-left: 4em;
}

.depot_form div {
    margin: 0.5em 0;
}

Đến đây giao diện sẽ như thế này:

capture

Định nghĩa validation

Chúng ta muốn đơn hàng được gửi lên phải thỏa mãn một số điều kiện, đó là không có trường nào được để trống. Chúng ta sửa lại file app/models/order.rb như sau:

class Order < ActiveRecord::Base
    PAYMENT_TYPES = [ "Cash on delivery", "Ngân lượng", "Bảo Kim", "Bank Card" ]
 
    validates :name, :address, :email, :pay_type, :presence => true
    validates :pay_type, :inclusion => PAYMENT_TYPES
end

Định nghĩa mỗi quan hệ

Một đối tượng LineItem sẽ thuộc về một đối tượng Order. Chúng ta sửa lại file model line_item.rb như sau:

class LineItem < ActiveRecord::Base
    belongs_to :order
    belongs_to :product
    belongs_to :cart
 
    def total_price
        product.price * quantity
    end
end

Ngược lại, một đối tượng Order sẽ có nhiều đối tượng LineItem trong đó, ngoài ra khi một đối tượng Order bị hủy thì các đối tượng LineItem “thuộc về” Order đó cũng sẽ bị hủy theo. Chúng ta sửa file model order.rb như sau:

class Order < ActiveRecord::Base
    has_many :line_items, :dependent => :destroy
    PAYMENT_TYPES = [ "Cash on delivery", "Ngân lượng", "Bảo Kim", "Bank Card" ]
 
    validates :name, :address, :email, :pay_type, :presence => true
    validates :pay_type, :inclusion => PAYMENT_TYPES
end

Tạo đơn hàng

Khi người dùng điền đầy đủ thông tin và click nút ‘Place order’, nút này sẽ gửi các thông tin đó đến phương thức create trong lớp OrdersController. Chúng ta sửa lại một tí như sau:

class OrdersController < ApplicationController
    .
    .
    .
    # POST /orders
    # POST /orders.json
    def create
        @order = Order.new(order_params)
        @order.add_line_items_from_cart(current_cart)
 
        respond_to do |format|
            if @order.save
                Cart.destroy(session[:cart_id])
                session[:cart_id] = nil
 
                format.html { redirect_to '/', notice: 'Thank you for your order' }
                format.json { render :show, status: :created, location: @order }
            else
                format.html { render :new }
                format.json { render json: @order.errors, status: :unprocessable_entity }
            end
        end
    end
    .
    .
    .
end

Phương thức mặc định của Rails là tạo một đối tượng Order với thông tin lấy từ các tham số do người dùng nhập vào. Tuy nhiên chưa có thông tin về các đối tượng LineItem, chúng ta thêm vào bằng phương thức add_line_items_from_cart, phương thức này sẽ được định nghĩa sau. Sau khi đã tạo đối tượng Order xong và lưu vào giỏ hàng thì chúng ta xóa giỏ hàng trong session đi.

Bây giờ chúng ta định nghĩa phương thức add_line_items_from_cart như sau:

class Order < ActiveRecord::Base
    has_many :line_items, :dependent => :destroy
    PAYMENT_TYPES = [ "Cash on delivery", "Ngân lượng", "Bảo Kim", "Bank Card" ]
 
    validates :name, :address, :email, :pay_type, :presence => true
    validates :pay_type, :inclusion => PAYMENT_TYPES
 
    def add_line_items_from_cart(cart) 
        cart.line_items.each do |item|
            item.cart_id = nil
            line_items << item
        end
    end
end

Phương thức này nhận vào tham số là một đối tượng Cart, chúng ta duyệt danh sách các đối tượng LineItem có trong Cart này, các đối tượng LineItem sẽ được gán giá trị cho thuộc tính cart_idnil để tránh báo lỗi khi xóa giỏ hàng, tiếp theo chúng ta đưa đối tượng LineItem đó vào mảng line_items.

Vậy là xong, bây giờ chúng ta có thể sử dụng chức năng đặt hàng được rồi.

capture

Và khi đặt hàng xong thì chúng ta được trỏ về trang ‘/’ với thông báo đặt hàng thành công (Thanh you for your order).

capture

Tuy nhiên nếu tiếp tục thêm hàng vào giỏ thì tin nhắn này vẫn tồn tại ở đó, chẳng qua là vì trình duyệt gửi lệnh AJAX để cập nhật giỏ hàng chứ không gửi lệnh HTTP để tải lại trang. Để ẩn dòng thông báo này thì chúng ta sửa lại file create.js.erb như sau:

var content = "<%= escape_javascript(render(current_cart)) %>";
document.getElementById("cart").innerHTML = content;

$("#current_item").css({'background-color':'#88ff88'});
$("#current_item").animate({backgroundColor:'#114411'}, 1000 );

var p_notice = document.getElementById("notice");
if(p_notice !== null)
    p_notice.remove();

Chỉ đơn giản là kiểm tra xem trang web có thẻ nào có idnotice hay không và xóa nó đi. Vậy là dòng thông báo sẽ biến mất.