Author Archives: Phở Code

Rails – Xác thực user – Phần 2

Trong phần này chúng ta sẽ tiếp tục xây dựng chức năng đăng nhập user.

Tạo controller

Đầu tiên chúng ta tạo 2 controller mới là sessionsadmin.

C:\Projects\Rails\depot>rails generate controller sessions new create destroy
...
C:\Projects\Rails\depot>rails generate controller admin index
...

Controller sessions sẽ có 3 phương thức là new, createdestroy. Trong đó:

  • new sẽ xử lý URL /login với phương thức GET, kết quả trả về trang hiển thị form đăng nhập
  • create sẽ xử lý URL /login với phương thức POST, trong đó kiểm tra thông tin username và password do người dùng gửi lên có chính xác không, sau đó lưu lại trong biến session rồi trả về trang /admin/index nếu đúng, không thì quay về lại trang /login (GET).
  • destroy sẽ xử lý URL /logout với phương thức DELETE, mục đích là để hủy biến session, tức là chức năng đăng xuất, sau đó trả về trang chủ là '/'.

Controller admin có một phương thức là index, xử lý URL /admin/index (GET), phương thức này sẽ hiển thị một số câu thông báo đơn giản chứ chưa làm gì nhiều ở đây.

Xử lý quá trình đăng nhập

Chúng ta sửa lại file sessions_controller.rb trong thư mục app/controllers như sau:

class SessionsController < ApplicationController
    def new 
    end

    def create 
        if user = User.authenticate(params[:name], params[:password])
            session[:user_id] = user.id
            redirect_to admin_url
        else
            redirect_to login_url, :alert => "Invalid username/password"
        end
    end

    def destroy
        session[:user_id] = nil
        redirect_to '/', :notice => "Logged out"
    end
end

Phương thức new để hiển thị form, chứ không xử lý gì cả nên chúng ta để không.

Phương thức create sẽ kiểm tra xem người dùng có đăng nhập đúng không bằng cách gọi phương thức authenticate trong lớp User mà chúng ta đã làm trong bài trước. Nếu đúng thì chúng ta tạo một phần tử trong biến session với key là :user_id và value là user.id, sau đó trả về trang 'admin/index' có trong biến admin_url.  Nếu user đăng nhập sai thì chúng ta trả về lại trang /login, kèm theo câu thông báo trong biến :alert.

Phương thức destroy sẽ xóa phần tử :user_id trong biến session, sau đó trả về trang '/', với câu thông báo trong biến :notice.

Tạo form đăng nhập

Như đã nói, form này được phương thức new trong lớp sessions_controller hiển thị từ file new.html.erb trong thư mục app/views/sessions, chúng ta sửa lại file này như sau:

<div class="depot_form">
    <% if flash[:alert] %>
        <p id="notice"><%= flash[:alert] %></p>
    <% end %>
 
    <% if notice %>
        <p id="notice"><%= notice %></p>
    <% end %>
    <%= form_tag '/login' do %>
        <fieldset>
            <legend>Log In</legend>
 
            <div>
                <label for="name">Name:</label>
                <%= text_field_tag :name, params[:name] %>
            </div>
 
            <div>
                <label for="password">Password:</label>
                <%= password_field_tag :password, params[:password] %>
            </div>
 
            <div>
                <%= submit_tag "Login" %>
            </div>
        </fieldset>
    <% end %>
</div>

Chúng ta hiển thị form và câu thông báo từ biến :alert nếu có (từ người dùng đã đăng nhập sai).

Khác với các phần trước là chúng ta tạo form từ model, tức là model có các trường gì thì chúng ta có thể tạo nhanh form cho model đó với phương thức helper là form_for. Còn ở đây chúng ta đang tạo form riêng, không có model nào cả, do đó chúng ta dùng phương thức helper là form_tag,

Phương thức form_tag có phương thức gửi mặc định là POST, URL mặc định là <tên_controller>/<tên_phương_thức>, tức là form này được tạo trong file new.html.erb (từ phương thức new trong controller sessions) thì mặc định URL gửi đi là /sessions/new. Nhưng ở đây chúng ta dùng URL riêng là /login luôn.

Bên trong chúng ta khai báo các trường bằng các phương thức helper khác như text_field_tag dùng để tạo textbox, password_field_tag dùng để tạo ô nhập password, submit_tag tạo nút Submit. Khi sử dụng các phương thức này thì chúng ta phải tự khai báo các idname.

Ngoài ra còn có rất nhiều các phương thức tạo nhanh form khác, bạn có thể xem tại đây.

Tạo trang admin/index

Ở đây trang /admin/index sẽ chưa làm gì nhiều, chúng ta sẽ tùy chỉnh sau:

<h1>Welcome</h1>

Time: <%= Time.now %> <br>
There are <%= pluralize(@total_orders, "order") %>.

<%= button_to 'Logout', '/logout', :method => :delete %>

Chúng ta cho hiển thị giờ hiện tại (trên máy tính) và tổng số lượng đơn hàng đã được tạo ra. Ở cuối trang là nút bấm dẫn đến URL '/logout' với phương thức gửi là DELETE.

Tổng số lượng đơn hàng được lấy từ biến @total_orders, do đó chúng ta khai báo biến này trong controller như sau:

class AdminController < ApplicationController
    def index
        @total_orders = Order.count
    end
end

Routing

Việc cần làm cuối cùng là phải tự tay điều hướng các URL mới vào các controller đã được tạo ở trên, bởi vì chúng ta tạo controller bằng tay chứ không được tạo tự động thông qua model như trước. Chúng ta sửa lại file config/routes.rb như sau:

Rails.application.routes.draw do
    get 'admin/index'

    get 'sessions/new'

    get 'sessions/create'

    get 'sessions/destroy'

    resources :users
    resources :orders
    resources :line_items
    resources :carts
    get 'store/index'

    resources :products do 
        get 'who_bought', :on => :member
    end
 
    root :to => 'store#index' 
 
    get 'admin' => 'admin#index'
 
    controller :sessions do
        get 'login' => :new
        post 'login' => :create
        delete 'logout' => :destroy
    end
end

Đối với trang /admin/index thì chúng ta dùng phương thức get rồi điều hướng vào phương thức index trong controller admin.

Đối với các controller có nhiều phương thức xử lý thì chúng ta có thể định nghĩa chúng gộp chung trong khối lệnh controller...do...end. Trong đó chúng ta cũng sử dụng các phương thức bình thường như get, post, delete, phương thức gửi đến có thể để theo dạng symbol chứ không cần phải khai báo rõ ra thành chuỗi (như 'sessions#new' hay 'sessions#create'…) nữa.

Bây giờ chúng ta có thể trỏ vào trang /login và đăng nhập được rồi.

Đăng nhập thành công:

Đăng xuất:

Rails – Xác thực user – Phần 1

Trong phần này chúng ta sẽ tạo cấu trúc lưu trữ thông tin user (người dùng).

Ở đây chúng ta chỉ lưu trữ đơn giản, bao gồm username và password, nhưng password sẽ không được lưu bình thường mà chúng ta sẽ mã hóa bằng thuật toán SHA2, do đó password trong CSDL sẽ gồm 2 thành phần là hashed (mật khẩu đã được mã hóa) và salt (giá trị salt).

Nếu bạn chưa biết về kiểu mã hóa này thì có thể hiểu đơn giản là khi người dùng gửi mật khẩu lên để đăng ký, chúng ta sẽ tạo ra một ký tự chuỗi ngẫu nhiên có độ dài cố định gọi là salt, sau đó dùng một thuật toán mã hóa (ở đây là SHA2) để mã hóa chuỗi mật khẩu với chuỗi salt để cho ra chuỗi hashed hay còn gọi là mật khẩu đã được mã hóa. Về thuật toán SHA2 thì ở đây chúng ta dùng thư viện, nếu bạn muốn biết chi tiết cách hoạt động thì có thể tìm hiểu trên mạng.

Định nghĩa model user

Đầu tiên chúng ta tạo model rồi cập nhật lên CSDL:

C:\Projects\Rails\depot>rails generate scaffold user name:string hashed_password:string salt:string
...
C:\Projects\Rails\depot>rake db:migrate

Bảng user của chúng ta sẽ gồm 3 trường là name, hashed_passwordsalt.

File user.rb được tạo ra, chúng ta sửa lại file này như sau:

require 'digest/sha2'

class User < ActiveRecord::Base
    validates :name, :presence => true, :uniqueness => true
    validates :password, :confirmation => true
    attr_accessor :password_confirmation
    attr_reader :password 
 
    validate :password_must_be_present
 
    def User.encrypt_password(password, salt) 
        Digest::SHA2.hexdigest(password + salt)
    end
 
    def password=(password) 
        @password = password
 
        if password.present?
            generate_salt
            self.hashed_password = self.class.encrypt_password(password, salt)
        end
    end
 
    def User.authenticate(name, password) 
        if user = find_by_name(name)
            if user.hashed_password == encrypt_password(password, user.salt)
               user
            end
        end
    end

private
    def password_must_be_present 
        if hashed_password.present? == false
            errors.add(:password, "Missing password")
        end
    end
 
    def generate_salt
        self.salt = self.object_id.to_s + rand.to_s
    end
end

Lớp này sẽ hơi phức tạp một chút.

require 'digest/sha2'

Ngay dòng đầu tiên chúng ta gọi thư viện SHA2, đây là thư viện có sẵn trong Ruby.

validates :name, :presence => true, :uniqueness => true
validates :password, :confirmation => true

Chúng ta kiểm tra xem biến name có tồn tại hay không, và name không được trùng nhau. Ngoài ra biến password phải được nhập 2 lần và cả 2 phải giống nhau, tức là khi chúng ta đăng ký thì người dùng gõ mật khẩu, sau đó phải gõ lần nữa để xác nhận.

attr_accessor :password_confirmation
attr_reader :password 

attr_accessor là phương thức tạo nhanh GetterSetter của Ruby, ở đây là biến password_confimation, tương tự attr_reader tạo Getter cho biến password.

validate :password_must_be_present

Ngoài ra chúng ta còn kiểm tra xem biến hashed_password có tồn tại hay không, tức là kiểm tra xem mật khẩu đã được mã hóa hay chưa. Chúng ta kiểm tra bằng cách định nghĩa phương thức password_must_be_present:

private
    def password_must_be_present 
        if hashed_password.present? == false
            errors.add(:password, "Missing password")
        end
    end

Nếu chưa có thì chúng ta cho báo lỗi.

Tiếp theo chúng ta định nghĩa hàm thực hiện mã hóa password:

    def User.encrypt_password(password, salt) 
        Digest::SHA2.hexdigest(password + salt)
    end
.
.
.
private
.
.
.
    def generate_salt
        self.salt = self.object_id.to_s + rand.to_s
    end

Hàm encrypt_password sẽ gọi hàm Digest::SHA2.hexdigest và nhận vào tham số là passwordsalt. Kết quả sẽ cho ra một chuỗi 64 kí tự (256 bit). Salt sẽ được tạo từ hàm generate_salt, chúng ta cho tạo chuỗi salt từ id của chính đối tượng User đó rồi nối với một chuỗi số ngẫu nhiên tạo từ hàm rand.

def password=(password) 
    @password = password
 
    if password.present?
        generate_salt
        self.hashed_password = self.class.encrypt_password(password, salt)
    end
end

Chúng ta định nghĩa lại toán tử = trên biến password, để khi người dùng gửi password lên, thì password sẽ không được gán thẳng vô biến mà sẽ được mã hóa lại rồi mới lưu vào CSDL.

def User.authenticate(name, password) 
    if user = find_by_name(name)
        if user.hashed_password == encrypt_password(password, user.salt)
           user
        end
    end
end

Chúng ta định nghĩa phương thức authenticate để xác thực người dùng, khi người dùng đăng nhập với namepassword, chúng ta mã hóa password đó với chuỗi salt của người dùng lưu trong CSDL và kiểm tra xem chuỗi vừa được mã hóa có giống như chuỗi đã được mã hóa trong CSDL không, nếu giống thì trả về đối tượng User, tức là đăng nhập thành công.

Tùy chỉnh controller

Chúng ta sửa lại một số phương thức trong lớp UsersController như sau:

class UsersController < ApplicationController
    .
    .
    .
    # POST /users
    # POST /users.json
    def create 
        @user = User.new(user_params)
 
        respond_to do |format|
            if @user.save 
                format.html { redirect_to users_url, notice: 'User was successfully created.' }
                format.json { render :show, status: :created, location: @user }
            else 
                format.html { render :new }
                format.json { render json: @user.errors, status: :unprocessable_entity }
            end
        end
    end
    .
    .
    .
    # PATCH/PUT /users/1
    # PATCH/PUT /users/1.json
    def update
        respond_to do |format|
            if @user.update(user_params)
                format.html { redirect_to users_url, notice: 'User was successfully updated.' }
                format.json { render :show, status: :ok, location: @user }
            else
                format.html { render :edit }
                format.json { render json: @user.errors, status: :unprocessable_entity }
            end
        end
    end
    .
    .
    .
    # Never trust parameters from the scary internet, only allow the white list through.
    def user_params
        params.require(:user).permit(:name, :hashed_password, :salt, :password, :password_confirmation)
    end
end

Chúng ta sửa lại 3 phương thức là update, createuser_params.

Hai phương thức updatecreate thì chúng ta chỉ sửa đơn giản là cho redirect về trang /users thôi, thay vì trang /users/<id>.

Còn phương thức user_params thì chúng ta sửa lại phương thức params.requrie().permit cho nhận thêm tham số passwordpassword_confirmation. Khi người dùng nhập form trên trình duyệt, sau đó bấm nút gửi thì trình duyệt sẽ gửi lệnh HTTP kèm theo các tham số lên server, các tham số đó sẽ nằm trong biến params, tùy nhiên mặc định Rails sẽ không chấp nhận các tham số được gửi lên vì lý do bảo mật, do đó nếu muốn một tham số nào được nhận thì chúng ta phải khai báo trong phương thức permit(). Trong số các tham số gửi lên khi đăng ký tài khoản có một tham số tên là user, tham số này lại gồm 3 tham số khác là name, passwordpassword_confirmation.

Mặc định Rails chỉ nhận các tham số có tên trùng với tên trong model thôi, tức là chỉ có name, hashed_passwordsalt, nhưng khi đăng ký tài khoản thì người dùng đâu có nhập hashed và salt mà chỉ nhập password bình thường. Do đó chúng ta phải đưa thêm tham số passwordpassword_confirmation thì Rails mới cho phép “đi qua” controller.

Tùy chỉnh view

Đầu tiên chúng ta sửa file index.html.erg trong thư mục app/views/users như sau:

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

<h1>Listing Users</h1>

<table>
    <thead>
        <tr>
            <th>Name</th>
            <th></th>
            <th></th>
            <th></th>
        </tr>
    </thead>

    <tbody>
        <% @users.each do |user| %>
        <tr>
            <td><%= user.name %></td> 
            <td><%= link_to 'Show', user %></td>
            <td><%= link_to 'Edit', edit_user_path(user) %></td>
            <td><%= link_to 'Destroy', user, method: :delete, data: { confirm: 'Are you sure?' } %></td>
        </tr>
        <% end %>
    </tbody>
</table>

<br>

<%= link_to 'New User', new_user_path %>

Mặc định file này hiển thị cả giá trị hashed và salt nữa, ở đây chúng ta bỏ các dòng đó đi.

Tiếp theo chúng ta sửa file _form.html.erb trong thư mục app/views/users như sau:

<div class="depot_form">

    <%= form_for @user do |f| %>
        <% if @user.errors.any? %>
            <div id="error_explanation">
                <h2><%= pluralize(@user.errors.count, "error") %> prohibited this user from being saved: </h2>
                <ul>
                    <% @user.errors.full_messages.each do |msg| %>
                        <li><%= msg %></li>
                    <% end %>
                </ul>
            </div>
        <% end %>
 
        <fieldset>
        <legend>Enter User Details</legend>
 
        <div>
            <%= f.label :name %>:
            <%= f.text_field :name, :size => 40 %>
        </div>
 
        <div>
            <%= f.label :password, 'Password' %>:
            <%= f.password_field :password, :size => 40 %>
        </div>
  
        <div>
            <%= f.label :password_confirmation, 'Confirm' %>:
            <%= f.password_field :password_confirmation, :size => 40 %>
        </div>
 
        <div>
            <%= f.submit %>
        </div> 
        </fieldset>
    <% end %>
</div>

Chúng ta sửa lại để hiển thị theo các lớp CSS chúng ta đã định nghĩa.

Bây giờ chúng ta có thể trỏ đăng ký tài khoản được rồi:

capture

Rails – Gửi email

Rails có sẵn một số lớp hỗ trợ gửi email, chúng ta sẽ tìm hiểu cách sử dụng các lớp này.

Chúng ta sẽ gửi mail thông báo xác nhận đã nhận được đơn hàng do người dùng đặt, bạn có thể sử dụng mail server của riêng mình hoặc có thể sử dụng dịch vụ mail nào khác đều được, ở đây mình sẽ sử dụng GMail cho đơn giản.

Có một lưu ý là vào năm 2014 Google đã quyết định khóa tính năng thao tác với email thông qua các ứng dụng kém bảo mật, do đó bạn phải truy cập vào https://www.google.com/settings/security/lesssecureapps và click chọn Turn on để cho phép các ứng dụng bên ngoài có thể truy cập từ xa được, ngoài ra nếu tài khoản của bạn có sử dụng xác thực 2 bước thì bạn cũng phải tắt tính năng này đi mới sử dụng được.

Cấu hình email

Thao tác cấu hình cũng không có gì nhiều để làm, trong thư mục config/environments có 3 file là development.rb, test.rbproduction.rb dùng để khai báo cấu hình tương ứng với từng môi trường mà chúng ta đã tìm hiểu. Chúng ta đang cấu hình cho môi trường phát triển nên sửa lại file development.rb như sau:

Rails.application.configure do
    # Settings specified here will take precedence over those in config/application.rb.

    # In the development environment your application's code is reloaded on
    # every request. This slows down response time but is perfect for development
    # since you don't have to restart the web server when you make code changes.
    config.cache_classes = false

    # Do not eager load code on boot.
    config.eager_load = false

    # Show full error reports and disable caching.
    config.consider_all_requests_local = true
    config.action_controller.perform_caching = false

    # Don't care if the mailer can't send.
    config.action_mailer.raise_delivery_errors = false

    # Print deprecation notices to the Rails logger.
    config.active_support.deprecation = :log

    # Raise an error on page load if there are pending migrations.
    config.active_record.migration_error = :page_load

    # Debug mode disables concatenation and preprocessing of assets.
    # This option may cause significant delays in view rendering with a large
    # number of complex assets.
    config.assets.debug = true

    # Asset digests allow you to set far-future HTTP expiration dates on all assets,
    # yet still be able to expire them through the digest params.
    config.assets.digest = true

    # Adds additional error checking when serving assets at runtime.
    # Checks for improperly declared sprockets dependencies.
    # Raises helpful error messages.
    config.assets.raise_runtime_errors = true

    # Raises error for missing translations
    # config.action_view.raise_on_missing_translations = true
 
    config.action_mailer.delivery_method = :smtp
 
    config.action_mailer.smtp_settings = {
        :address => 'smtp.gmail.com',
        :port => 587,
        :domain => 'phocode.com',
        :authentication => 'plain',
        :user_name => 'phocode7@gmail.com',
        :password => '<mật khẩu>',
        :enable_starttls_auto => true
    }
end

Việc gửi mail sẽ sử dụng phương thức SMTP, các thông số khác như địa chỉ server, port... của GMail là smtp.gmail.com trên cổng 587

Tạo ActionMailer

Đầu tiên chúng ta chạy lệnh rails generate mailer như sau:

C:\Projects\Rails\depot>rails generate mailer Notifier order_received
...

Lệnh trên sẽ tạo một lớp có tên Notifier kế thừa từ lớp ActionMailer::Base, nằm trong file notifier.rb trong thư mục app/mailers. Trong lớp này có phương thức order_received do chúng ta khai báo. Mặc định file này có nội dung như sau:

class Notifier < ApplicationMailer
 
    # Subject can be set in your I18n file at config/locales/en.yml
    # with the following lookup:
    #
    # en.notifier.order_received.subject
    #
    def order_received
        @greeting = "Hi"   
 
        mail to: "to@example.org"
    end
end

Chúng ta chỉnh sửa lại một chút như sau:

class Notifier < ApplicationMailer
    default :from => "Pho Code <phocode7@gmail.com>"

    def order_received(order)    
        @order = order
 
        mail :to => @order.email, :subject => "We've received your order" 
    end
end

Chúng ta cho phương thức này nhận vào tham số là một đối tượng Order vì chỉ có lớp này mới lưu các thông tin về người đặt hàng.

Phương thức mail sẽ thực sự thực hiện việc gửi email, phương thức này nhận vào các tham số:

  • to: địa chỉ người nhận
  • from: địa chỉ người gửi
  • subject: tiêu đề
  • cc: Carbon copy (không bắt buộc)

Tuy nhiên ở đây chúng ta chỉ truyền vào 2 tham số là tosubject thôi, còn from thì chúng ta khai báo là ở dòng default :from... ở đầu file. Ý nghĩa của dòng này là bất kỳ phương thức mail nào cũng sẽ sử dụng tham số :from được khai báo trong dòng default.

Mail Template

Tuy nhiên bạn sẽ nhận ra thành phần còn thiếu đó là nội dung mail. Nội dung sẽ nằm trong file có tên order_received.text.erb hoặc order_received.html.erb trong thư mục app/views/notifier, 2 file này được tạo tự động khi chúng ta chạy lệnh rails generate mailer.

Các file này cũng được gọi là file template giống như Controller thì có View vậy, chỉ khác là ở đây các file này dùng để hiển thị nội dung cho email. File có đuôi .text.erb dùng để hiển thị email dạng text, tức là không thể dùng các thẻ HTML để làm cho có màu mè được.

Mặc định thì Rails sẽ sử dụng template trong file .html.erb.

Chúng ta sửa lại file order_received.html.erb như sau:

<h3>Books Store</h3>
<p>
    Dear <b><%= @order.name %></b>,
</p>

<p>
    Thank you for your order, your items:
</p>
<table>
    <tr>
        <th colspan="2">Quantity</th>
        <th>Description</th>
    </tr>
    <%= render @order.line_items %>
</table>

<p>Your payment type: <b><%= @order.pay_type %></b></p>
<p>Thanks for shopping with <b>Books Store<b> and best regards.</p>

Chúng ta có thể gọi các hàm helper, gọi hàm render như bình thường.

Gửi email

Chúng ta sẽ cho gửi email khi người dùng tạo Order thành công, do đó chúng ta sửa lại phương thức create trong lớp OrdersController 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
                Notifier.order_received(@order).deliver
 
                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

Để thực hiện việc gửi mail thì chúng ta gọi phương thức order_received() đã định nghĩa ở trên, sau đó gọi phương thức deliver().

Vậy là xong, bây giờ mỗi khi người dùng đặt hàng thì sẽ có mail gửi tới người dùng đó.

mailer

Rails – Phân trang với will_paginate

Trong bài này chúng ta sẽ thực hiện chức năng phân trang.

Trong Rails không có hàm hay lớp nào có thể thực hiện chức năng phân trang được. Tuy nhiên do Rails là mã nguồn mở nên cũng có cộng đồng hỗ trợ rất lớn, rất nhiều thư viện được viết ra để hỗ trợ chúng ta. Các thư viện hỗ trợ phân trang cũng rất nhiều, ở đây chúng ta sẽ sử dụng thư viện will_paginate, có địa chỉ github tại https://github.com/mislav/will_paginate.

Chúng ta không cần phải tự tải về rồi biên dịch gì hết. Mặc định Rails có phần mềm quản lý các thư viện này rồi, đó là phần mềm Gem. Chúng ta sẽ tìm hiểu cách sử dụng gem sau.

Cài đặt will_paginate

Đầu tiên chúng ta khai báo thư viện này trong file Gemfile, file này không có phần mở rộng và nằm ở thư mục gốc của project:

source 'http://rubygems.org'


# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '4.2.5.1'
# Use sqlite3 as the database for Active Record
gem 'sqlite3'
# Use SCSS for stylesheets
gem 'sass-rails', '~> 5.0'
# Use Uglifier as compressor for JavaScript assets
gem 'uglifier', '>= 1.3.0'
# Use CoffeeScript for .coffee assets and views
gem 'coffee-rails', '~> 4.1.0'
# See https://github.com/rails/execjs#readme for more supported runtimes
# gem 'therubyracer', platforms: :ruby

# Use jquery as the JavaScript library
gem 'jquery-rails'
# Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks
gem 'turbolinks'
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
gem 'jbuilder', '~> 2.0'
# bundle exec rake doc:rails generates the API under doc/api.
gem 'sdoc', '~> 0.4.0', group: :doc

# Use ActiveModel has_secure_password
# gem 'bcrypt', '~> 3.1.7'

# Use Unicorn as the app server
# gem 'unicorn'

# Use Capistrano for deployment
# gem 'capistrano-rails', group: :development

group :development, :test do
 # Call 'byebug' anywhere in the code to stop execution and get a debugger console
 gem 'byebug'
end

group :development do
 # Access an IRB console on exception pages or by using <%= console %> in views
 gem 'web-console', '~> 2.0'
end

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

gem 'will_paginate', '>= 3.0'

Phiên bản mới nhất được phát hành vào tháng 10/2016 là phiên bản 3.1.5. Chúng ta khai báo dòng gem 'will_paginate', '>= 3.0' cho biết chúng ta muốn dùng phiên bản 3.0 trở đi, nhưng không dùng 4.0.

Tiếp theo chúng ta cài thư viện này bằng lệnh bundle install:

C:\Projects\Rails\depot>bundle install
...
Installing will_paginate 3.1.5
...

Vậy là xong, bạn có thể sẽ cần phải khởi động lại server nếu có đang chạy.

Tạo dữ liệu mẫu

Chúng ta tạo 100 bản ghi Order để làm dữ liệu test. Thay vì tự ngồi gõ từng form đặt hàng thì chúng ta có thể chạy đoạn code có sẵn.

Chúng ta tạo một file có tên create_dummy_orders.rb nằm trong thư mục bin có nội dung như sau:

Order.transaction do
    (1..100).each do |i|
        Order.create(:name => "Customer #{i}", 
                     :address => "#{i} Street",
                     :email => "customer_#{i}@phocode.com",
                     :pay_type => "Bank Card")
    end
end

Đoạn code trên sẽ tạo 100 bản ghi Order. Để chạy đoạn code này thì chúng ta chạy lệnh rails runner bin/create_dummy_orders.rb:

C:\Projects\Rails\depot>rails runner bin/create_dummy_orders.rb

Lưu ý là đây không phải dữ liệu seed như chúng ta đã từng làm.

Bây giờ bạn có thể vào trang /orders và thấy danh sách 100 đơn hàng được hiển thị đầy đủ.

Phân trang

Chúng ta sẽ phân trang để cho hiển thị 10 đơn hàng trên một trang, đầu tiên chúng ta sửa lại phương thức index trong lớp OrdersController như sau:

class OrdersController < ApplicationController
    .
    .
    .
    # GET /orders
    # GET /orders.json
    def index   
        @orders = Order.paginate(:page => params[:page], :per_page => 10).order('created_at desc')
    end
    .
    .
    .
end

Thư viện will_paginate cho phép chúng ta sử dụng một phương thức có tên là paginate trên các đối tượng lưu trữ theo dạng danh sách. Phương thức này nhận vào tham số trang, ở đây là :page, tức là khi chúng ta xem các trang thì URL sẽ có dạng /orders?page=1, /orders?page=2… đây là tham số mặc định do thư viện này quy định. Phương thức này còn nhận vào tham số :per_page là số lượng “item” được hiển thị trên mỗi trang.

Ngoài ra chúng ta có thể sắp xếp các phần tử này bằng phương thức order, phương thức này nhận vào một chuỗi có dạng "<tên_trường> asc|desc", và các phần tử sẽ được sắp xếp dựa theo tên trường, ASC là sắp xếp từ A-Z, DESC thì ngược lại. Ở đây chúng ta sắp xếp theo trường created_at với thứ tự là DESC.

Cuối cùng trong file View app/views/orders/index.html.erb chúng ta sửa lại như sau:

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

<h1>Listing Orders</h1>

<table>
    <thead>
        <tr>
            <th>Name</th>
            <th>Address</th>
            <th>Email</th>
            <th>Pay type</th>
            <th colspan="3"></th>
        </tr>
    </thead>

    <tbody>
        <% @orders.each do |order| %>
            <tr>
                <td><%= order.name %></td>
                <td><%= order.address %></td>
                <td><%= order.email %></td>
                <td><%= order.pay_type %></td>
                <td><%= link_to 'Show', order %></td>
                <td><%= link_to 'Edit', edit_order_path(order) %></td>
                <td><%= link_to 'Destroy', order, method: :delete, data: { confirm: 'Are you sure?' } %></td>
            </tr>
        <% end %>
    </tbody>
</table>

<br>

<%= link_to 'New Order', new_order_path %>

<p><%= will_paginate @orders %></p>

Chúng ta gọi hàm helper là will_paginate với tham số là @orders để tạo các thẻ <a> dẫn tới từng trang cụ thể.

Vậy là xong, bây giờ trang /orders sẽ hiển thị như thế này:

capture

Rails – Xây dựng tính năng RSS

RSS là công nghệ hỗ trợ người dùng cập nhật nội dung từ website mà không cần phải vào trang web. Cách hoạt động thì đơn giản là website sẽ tạo ra một file có định dạng XML rồi trả về cho người dùng, người dùng sẽ dùng một phần mềm để đọc file đó.

RSS cũng có nhiều phiên bản khác nhau, phổ biến nhất là RSS 1.0, RSS 2.0 và Atom. Cái nào cũng có ưu/nhược điểm riêng và đều được sử dụng rộng rãi, có website dùng cả 3 phiên bản.

Riêng Ruby có thư viện hỗ trợ tạo file RSS ở 3 phiên bản trên và các phiên bản khác nhỏ lẻ khác. Rails cũng có hàm tạo RSS dựa trên thư viện của Ruby, và mặc định Rails chọn phiên bản Atom.

Chúng ta sẽ tạo một file Atom hiển thị những đơn hàng đã mua sản phẩm có id bất kỳ.

Định nghĩa Action

Đầu tiên chúng ta định nghĩa phương thức who_bought trong lớp ProductsController như sau:

class ProductsController < ApplicationController
    .
    .
    .
    def who_bought 
        @product = Product.find(params[:id]) 
        respond_to do |format|
            format.atom
            format.xml { render :xml => @product }
        end
    end
    .
    .
    .
end

Phương thức này sẽ được gọi từ URL là /products/<id>/who_bought.atom . Ở đây chúng ta lấy đối tượng Productid tương ứng trong tham số gửi lên. Sau đó gọi hàm format.atom, hàm này sẽ chạy đoạn code có trong file <tên_phương_thức>.atom.builder nằm trong thư mục app/views/<tên model> dùng để tạo dữ liệu Atom, tức là ở đây sẽ gọi tới file who_bought.atom.builder trong thư mục app/views/products. Chúng ta truyền vào file này biến @product đã tạo ra ở trên trong phương thức format.xml.

Tạo dữ liệu Atom

Tiếp theo chúng ta tạo file who_bought.atom.builder trong thư mục app/views/products có nội dung như sau:

atom_feed do |feed|
    feed.title "Who bought #{@product.title}" 
 
    latest_order = @product.orders.sort_by(&:updated_at).last
    feed.updated(latest_order && latest_order.updated_at)
 
    @product.orders.each do |order|
        feed.entry(order) do |entry|
            entry.title "Order #{order.id}"
            entry.summary :type => 'xhtml' do |xhtml|
                xhtml.p "Shipped to #{order.address}"
 
                xhtml.table do
                    xhtml.tr do
                        xhtml.th 'Product'
                        xhtml.th 'Quantity'
                        xhtml.th 'Total Price'
                    end
 
                    order.line_items.each do |item|
                        xhtml.tr do
                            xhtml.td item.product.title
                            xhtml.td item.quantity
                            xhtml.td number_to_currency item.total_price
                        end
                    end
 
                    xhtml.tr do
                        xhtml.th 'Total', :colspan => 2
                        xhtml.th number_to_currency order.line_items.map(&:total_price).sum
                    end
                end
                xhtml.p "Paid by #{order.pay_type}"
            end 
    
            entry.author do |author|
                entry.name order.name
                entry.email order.email
            end
        end
    end
end

Hàm atom_feed là hàm helper tạo ra code XML theo định dạng Atom.

atom_feed do |feed|
...
end

Hàm này tạo ra một đối tượng ActionView::Helpers::AtomFeedHelper::AtomFeedBuilder, ở đây là biến feed.

feed.title "Who bought #{@product.title}" 
 
latest_order = @product.orders.sort_by(&:updated_at).last
feed.updated(latest_order && latest_order.updated_at)

Đầu tiên chúng ta gọi phương thức titleupdated để thiết lập tiêu đề và thời gian tạo gần đây nhất bằng cách lấy thời gian trong thuộc tính updated_at của bản ghi Order cuối cùng. Nếu không tìm thấy thì Rails sẽ lấy thời gian là giờ hiện tại trên máy. Hai phương thức này sẽ tạo thẻ <entry><title> trong file Atom.

@product.orders.each do |order|
    feed.entry(order) do |entry|
    ...
    end
end

Tiếp theo chúng ta lặp từng bản ghi Order, cứ mỗi lần lặp thì chúng ta gọi phương thức feed.entry. Phương thức này nhận vào một đối tượng bất kỳ và tạo ra biến ActionView::Helpers::AtomFeedHelper::AtomBuilder, ở đây là biến entry.

Chúng ta dùng biến entry này để tạo các thẻ html như bình thường. Bạn có thể dựa vào code là có thể tự đoán phương thức nào tạo thẻ nào rồi.

Thiết lập mối quan hệ

Tuy nhiên chúng ta chưa định nghĩa mối quan hệ giữa một đối tượng Product và một đối tượng Order. Nhưng một đối tượng Order lại chứa nhiều đối tượng LineItem, một đối tượng Product cũng có nhiều đối tượng LineItem, do đó chúng ta có thể liên kết đối tượng Product với đối tượng Order thông qua đối tượng LineItem như sau:

class Product < ActiveRecord::Base
    validates :title, :description, :image_url, :presence => true
    validates :price, :numericality => {:greater_than_or_equal_to => 1.0}
    validates :title, :uniqueness => true
    validates :image_url, :format => {
        :with => %r{\.(gif|jpg|png)\Z}i,
        :message => 'Chi nhan file GIF, JPG, PNG'
    }
 
    has_many :line_items
    has_many :orders, :through => :line_items
    before_destroy :check_if_has_line_item
 
private
 
    def check_if_has_line_item
        if line_items.empty?
            return true
        else
            errors.add(:base, 'This product has a LineItem')
            return false
        end
    end
end

Routing

Cuối cùng chúng ta điều hướng URL tới phương thức who_bought như sau:

Rails.application.routes.draw do
    resources :orders
    resources :line_items
    resources :carts
    get 'store/index'

    resources :products do 
        get :who_bought, :on => :member
    end
 
    root :to => 'store#index' 
end

Nếu bạn còn nhớ thì hàm resources sẽ tự động định nghĩa 7 URL vào 7 phương thức CRUD cơ bản.

Rails cho phép chúng ta điều hướng thêm vào sau các URL có sẵn, bằng cách khai báo thêm khối lệnh do...end sau phương thức resources và khai báo các dòng routing trong đó.

resources :products do
    get :who_bought, :on => :member
end

Dòng code trên sẽ gắn URL /who_bought vào URL của các phương thức đã định nghĩa trước đó, tức là Rails sẽ hiểu các URL như /products/1/who_bought. Và gửi tới phương thức who_bought trong lớp ProductsController.

Vậy là xong, bạn có thể chạy server và trỏ vào URL như /products/1/who_bought.atom là sẽ có đoạn code XML hiển thị danh sách các đơn hàng có sản phẩm có product_id1. Lưu ý là vì URL /who_bought trả về nội dung Atom chứ không phải HTML như bình thường nên khi trỏ thì chúng ta thêm đuôi .atom ở cuối URL.

capture

Nếu muốn bạn có thể dùng các phần mềm đọc tin RSS để đọc file Atom này cũng được. Dưới đây là hình khi dùng RSS Feed Reader, đây là một extension của trình duyệt Chrome.

bbbcapture

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.

Rails – Tạo phương thức Helper

Chúng ta đã biết các phương thức helper là các phương thức có thể được gọi trong file View (.html.erb), trong bài render chúng ta cũng biết là có một cách để “biến” một phương thức bình thường thành một phương thức controller bình thường thành một phương thức helper là khai báo trong phương thức helper_method.

Trong bài này chúng ta sẽ định nghĩa phương thức helper rõ ràng hơn.

Trong thư mục app của project, có một thư mục có tên là helpers, tất cả các lớp controller được tạo ra sẽ có một file có tên dạng <tên controller>_helper.rb nằm trong thư mục này. Ngoài ra khi tạo project thì còn có một file nữa là application_helper.rb. Đây là các file Rails tạo cho chúng ta để định nghĩa các hàm helper.

helpers

Chúng ta sẽ viết hàm ẩn thẻ <div id="cart"> nếu giỏ hàng không có sản phẩm nào.

Đầu tiên chúng ta sửa lại file application.html.erb như sau:

<!DOCTYPE html>
<html>
<head>
<title>Books Store</title> 
    <script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
    <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js"></script>
    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
    <%= csrf_meta_tags %>
</head>
<body id="store">
    <div id="banner">
        <%= image_tag("logo.png") %>
        <%= @page_title || "Books Store" %>
    </div>
    <div id="columns">
        <div id="side">
            <div id="cart"> 
                <%= hide_cart_if(current_cart.line_items.empty?, :id => "cart") do %>
                <%= render current_cart %> 
                <% end %>
            </div>
 
            <a href="#">Home</a><br />
            <a href="#">FAQ</a><br />
            <a href="#">News</a><br />
            <a href="#">Contact</a><br />
        </div>
        <div id="main">
            <%= yield %>
        </div>
    </div> 
</body>
</html>

Chúng ta bọc đoạn <%= render current_cart %> bằng <%= hide_cart_if() do %>...<% end %>. Chúng ta truyền vào hàm hide_cart_if 2 tham số, mục đích là để phương thức này biết giỏ hàng có trống không và nếu trống thì ẩn thẻ nào.

Tiếp theo chúng ta định nghĩa phương thức hide_cart_if trong file application_helper.rb như sau:

module ApplicationHelper
    def hide_cart_if(condition, attributes = {}, &block) 
        if condition
            attributes["style"] = "display:none"
        end
        content_tag("div", attributes, &block) 
    end
end

Tham số condition được truyền vào ở đây cho biết giỏ hàng có trống không. Tham số attributes là một danh sách các tham số khác, ở trên chúng ta truyền vào là id có giá trị "cart". Tham số &block là đoạn <%= render current_cart %>.

Nếu conditiontrue, tức là giỏ hàng đúng là trống, thì chúng ta định nghĩa thêm một phần tử trong thuộc tính attributes có khóa là style và giá trị là "display:none".

Cuối cùng chúng ta gọi phương thức content_tag(), hàm này nhận vào tên một thẻ, danh sách các thuộc tính và giá trị của tham số đó, và biến &block, hàm này sẽ bọc những gì trong &block với tên thẻ và thuộc tính. Tức là bây giờ đoạn code hiển thị giỏ hàng sẽ có dạng như thế này:

.
.
.
<div id="cart">
    <div style="display:none">
    <%= render current_cart %>
    </div>
</div>
.
.
.

Tức là không hiển thị ra nữa.

Chúng ta cũng sửa lại phương thức destroy trong lớp CartsController để khi xóa giỏ hàng thì được trỏ về trang '/' luôn, không trở lại trang /carts nữa:

class CartsController < ApplicationController
    .
    .
    .
    # DELETE /carts/1
    # DELETE /carts/1.json
    def destroy
        @cart = current_cart
        @cart.destroy
        session[:cart_id] = nil
 
        respond_to do |format|       
            format.html { redirect_to '/' }
            format.json { head :no_content } 
        end
    end
    .
    .
    .
end

Vậy là xong.

Rails – Sử dụng jQuery

jQuery là một thư viện kiểu mới của JavaScript, được tạo bởi John Resig vào năm 2006 với một phương châm tuyệt vời: Write less, do more – Viết ít hơn, làm nhiều hơn. jQuery làm đơn giản hóa việc truyền tải HTML, xử lý sự kiện, tạo hiệu ứng động và tương tác Ajax. (Theo Vietjack)

Trong phần này chúng ta sẽ sử dụng jQuery để đổi màu nền cho giỏ hàng khi thêm mới sản phẩm.

Đầu tiên chúng ta sửa phương thức create của lớp LineItemsController như sau:

class LineItemsController < ApplicationController
    .
    .
    .
    # POST /line_items
    # POST /line_items.json
    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('/', :notice => 'Line item was successfully created') } 
                format.js   { @current_item = @line_item }
                format.json { render :show, status: :created, location: @line_item }
            else
                format.html { render :new }
                format.json { render json: @line_item.errors, status: :unprocessable_entity }
            end
        end
    end
    .
    .
    .
end

Chúng ta truyền thêm biến instance là @current_item có giá trị là biến @line_item đã khai báo ở trên.

Tiếp theo chúng ta sửa file partial _line_item.html.erb trong thư mục app/views/line_items như sau:

<% if @current_item != nil %>
    <% if @current_item.id == line_item.id %>
<tr id="current_item">
    <% else %>
<tr>
    <% end %>
<% end %>
    <td><%= line_item.quantity%>x</td>
    <td><%= line_item.product.title %></td>
    <td class="item_price"><%= number_to_currency(line_item.total_price) %></td>
</tr>

Mục đích của đoạn code trên là gán thuộc tính id có giá trị là "current_item" vào thẻ <tr> nào đang hiển thị sản phẩm vừa được thêm vào.

Mỗi lần file partial này được gọi thì đầu tiên chúng ta kiểm tra xem biến @current_item được truyền qua có phải nil hay không, bởi vì file partial này còn được gọi từ nhiều nơi khác nữa mà biến @current_item không tồn tại. Tiếp theo chúng ta kiểm tra xem biến @currrent_item này có id giống với biến line_item đang được dùng để hiển thị hay không, vì trong một giỏ hàng có nhiều sản phẩm, nhưng sản phẩm vừa được thêm vào thì chỉ có một loại, và nếu trùng id thì chúng ta khai báo thẻ <tr>idcurrent_item, ngược lại thì không có.

Tiếp theo chúng ta sửa file create.js.erb trong thư mục app/views/carts 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 );

Chúng ta sử dụng jQuery để tạo hiệu ứng phát sáng màu nền rồi giảm dần trong 1 giây cho thẻ <tr> có thuộc tính idcurrent_item. Nếu bạn chưa từng làm việc với jQuery thì tìm hiểu thêm trên mạng, ở đây mình không giải thích.

Để có thể sử dụng thì chúng ta cần khai báo jQuery trong file application.html.erb như sau:

<!DOCTYPE html>
<html>
<head>
<title>Books Store</title> 
    <script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
    <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js"></script>
    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
    <%= csrf_meta_tags %>
</head>
<body id="store">
    <div id="banner">
        <%= image_tag("logo.png") %>
        <%= @page_title || "Books Store" %>
    </div>
    <div id="columns">
        <div id="side">
        <div id="cart"> 
            <%= render current_cart %>
        </div>
 
        <a href="#">Home</a><br />
        <a href="#">FAQ</a><br />
        <a href="#">News</a><br />
        <a href="#">Contact</a><br />
    </div>
    <div id="main">
        <%= yield %>
    </div>
    </div> 
</body>
</html>

Kết quả sẽ được như hình sau:

untitled

Rails – AJAX trong Rails

Nếu bạn chưa biết AJAX là gì thì có thể hiểu đơn giản đây là một công nghệ của Javascript, hỗ trợ chúng ta trao đổi dữ liệu giữa server và client (trình duyệt) mà không cần phải gửi các lệnh HTTP thông thường, tức là không cần phải load lại trang web.

Chúng ta sẽ áp dụng AJAX vào nút ‘Add to cart’ để thêm sản phẩm vào giỏ hàng mà không cần phải load lại trang.

Ở phía client, chúng ta sửa lại file index.html.erb trong thư mục app/views/store như sau:

<% if notice %>
<p id="notice"><%= notice %></p>
<% end %>
 
<h1>Product List</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),
                          :remote => true %>
        </div>
    </div>
<% end %>

Ở hàm button_to, chúng ta thêm thuộc tính :remotetrue, và khi bấm vào nút thì trình duyệt sẽ gởi một lời gọi AJAX, thay vì một lời gọi HTTP như bình thường.

Nút này vẫn sẽ gửi tới hàm create trong lớp LineItemsController, do đó ở phía server chúng ta sửa lại như sau:

class LineItemsController < ApplicationController
    .
    .
    .
    # POST /line_items
    # POST /line_items.json
    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('/', :notice => 'Line item was successfully created') }
                format.js 
                format.json { render :show, status: :created, location: @line_item }
            else
                format.html { render :new }
                format.json { render json: @line_item.errors, status: :unprocessable_entity }
            end
        end
    end
    .
    .
    .
end

Dòng format.js sẽ làm công việc trả lời cho các lời gọi AJAX, mặc định dữ liệu trả về sẽ được gửi trong một file Javascript có tên <tên_phương_thức_routing>.js.erb trong thư mục app/views/<tên_controller>, tức là ở đây sẽ trả về file create.js.erb trong thư mục app/views/line_items.

Do đó chúng ta tạo file create.js.erb có nội dung như sau:

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

Nếu bạn đã từng làm việc với Javascript thì đoạn code trên cũng không có gì khó hiểu, chỉ khác là chúng ta có thể gọi thêm cả code ERB của Rails trong này. Ở đây chúng ta thay thế nội dung trong thẻ có idcart với biến đã được khai báo là content. Biến này chúng ta truyền vào giá trị lấy từ hàm render current_cart.

Vậy là xong, bây giờ nút ‘Add to cart’ sẽ gửi các lời gọi AJAX thay cho lời gọi HTTP bình thường. Bạn có thể kiểm tra bằng cách để ý icon trên tab của trình duyệt không hiển thị thành vòng tròn xoay, hoặc rõ hơn là lời nhắn thêm sản phẩm thành công của biến :notice'Line item was successfully created' không hiện ra khi chúng ta thêm sản phẩm mới.

Không có AJAX:

bbcapture

Có AJAX:

cc

Rails – Hàm render

Hiện tại thì trang /carts/<id> sẽ hiển thị giỏ hàng đang được lưu trong session của trình duyệt người dùng bằng phương thức show trong lớp CartController, phương thức này sẽ “vẽ” những gì được định nghĩa trong file show.html.erb trong thư mục views/carts. 

Trong file này chúng ta “vẽ cứng” cách hiển thị thông tin giỏ hàng như sau:

<%= notice %>
<h2>Your cart</h2>
<table>
    <% @cart.line_items.each do |item| %>
        <tr>
            <td><%= item.quantity %> x </td>
            <td><%= item.product.title %></td>
            <td class="item_price"><%= number_to_currency(item.total_price) %></td>
        </tr>
    <% end %>
    
    <tr class="total_line">
        <td colspan="2">Total</td>
        <td class="total_cell"><%= number_to_currency(@cart.total_price) %></td>
    </tr>
</table>
 
<%= button_to 'Empty cart', 
              @cart, 
              :method => :delete,
              data: {:confirm => 'Are you sure?'} %>

Tức là đoạn <table>...</table> được code trực tiếp trong file này, trong phần này chúng ta sẽ tách đoạn này ra lưu trong một file khác rồi gọi đến file đó khi cần, bạn có thể hình dung nó giống như là một layout thu nhỏ vậy.

Để có thể gọi đến đoạn code được tách ra thì chúng ta dùng hàm helper có tên là render, hàm này có thể hiển thị một file template (file .html.erb), chuỗi dữ liệu, XML, JSON…v.v

Đầu tiên chúng ta sửa file show.html.erb ở trên như sau:

<%= render @cart %>

Bây giờ file này chỉ có một dòng duy nhất là lời gọi hàm render đến biến @cart, đây là biến lưu trữ thông tin của giỏ hàng được truyền lên từ tham số trong URL /carts/<id>, Rails sẽ tự hiểu là hiển thị đoạn code có trong file _cart.html.erb trong thư mục views/carts, tức là cứ truyền vào một đối tượng thì Rails sẽ gọi đến file có tên dạng _<tên lớp đối tượng>.html.erb (chú ý có dấu gạch dưới), ngoài ra nếu tên đối tượng truyền vào có kí tự ‘s’ ở cuối tên (số nhiều trong tiếng Anh) thì tên file cũng bỏ kí tự này luôn, tức là nếu chúng ta truyền vào @carts thì file này vẫn là _cart.html.erb. Các file như này có tên gọi chung là file partialcác file này Rails không tự tạo nên chúng ta phải tạo bằng tay.

Bây giờ chúng ta tạo file partial _cart.html.erb có nội dung 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 'Empty cart', 
              cart, 
              :method => :delete,
              data: {:confirm => 'Are you sure?' } %>

Vẫn là đoạn code cũ, chỉ có khác một chỗ là đoạn hiển thị danh sách các sản phẩm cùng với số lượng và tổng tiền của chúng thì chúng ta lại gọi một hàm render đến một file partial khác, ở đây chúng ta truyền vào biến cart.line_items. Khi biến @cart được truyền vào file _cart.html.erb, chúng ta tham chiếu đến biến này với cái tên trùng với file partial này, tức là _cart.html.erb thì chúng ta tham chiếu tới biến cart, không có dấu '@' nữa.

Và ở đây chúng ta truyền vào hàm render đối tượng cart.line_items, đây là một đối tượng lưu trữ dạng danh sách/tập hợp nhiều phần tử, cũng tương tự như trên, chỉ khác là Rails sẽ lặp qua danh sách đó, với mỗi lần lặp thì Rails lại gọi file partial tương ứng là _line_item.html.erb. Do đây là các đối tượng thuộc lớp LineItem nên file partial cũng nằm trong thư mục line_items cho dù có hàm gọi nó ở bên thư mục khác.

Bây giờ chúng ta tạo file _line_item.html.erb trong thư mục app/views/line_items như sau:

<tr>
    <td><%= line_item.quantity%>x</td>
    <td><%= line_item.product.title %></td>
    <td class="item_price"><%= number_to_currency(line_item.total_price) %></td>
</tr>

Chúng ta hiển thị số tiền, tên sản phẩm và tổng tiền như bình thường.

Bạn có thể chạy lại và thấy giao diện vẫn hiển thị như cũ, chỉ khác là bây giờ các phần được hiển thị ở các file khác nhau chứ không được code “cứng” trong một file nữa.

aacapture

Và để tận dụng khả năng phân tách này thì chúng ta hiển thị giỏ hàng ở bên sidebar của layout luôn. Chúng ta sửa lại file application.html.erb như sau:

<!DOCTYPE html>
<html>
<head>
<title>Books Store</title>
    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
    <%= csrf_meta_tags %>
</head>
<body id="store">
    <div id="banner">
        <%= image_tag("logo.png") %>
        <%= @page_title || "Books Store" %>
    </div>
    <div id="columns">
        <div id="side">
            <div id="cart">
                <%= render @cart %>
            </div>

            <a href="#">Home</a>
            <a href="#">FAQ</a>
            <a href="#">News</a>
            <a href="#">Contact</a>
        </div>
        <div id="main">
            <%= yield %>
        </div>
    </div>
</body>
</html>

Ở đây chúng ta cũng gọi hàm render với tham số là biến @cart để hiển thị từ file _cart.html.erb. Tuy nhiên khác với lời gọi từ file show.html.erb là có kèm theo tham số sau URL là /carts/<id>, ở đây lời gọi tại file application.html.erb sẽ gọi phương thức index trong lớp StoreController, phương thức này đã được cấu hình để nhận URL '/' nếu bạn còn nhớ, và phương thức này không nhận tham số nào cả, do đó trong phương thức này chúng ta phải tạo một đối tượng @cart để sử dụng, chúng ta sửa lại file này như sau:

class StoreController < ApplicationController
    def index
        @products = Product.all
        @cart = current_cart
    end
end

Tiếp theo chúng ta định nghĩa thêm một vài lớp CSS:

.
.
.
/* Cart sidebar Style */
#cart, #cart table {
    font-size: smaller;
    color: white;
} 

#cart table {
    border-top: 1px dotted #595;
    border-bottom: 1px dotted #595;
    margin-bottom: 10px;
}

Bây giờ mỗi khi bấm nút ‘Add to Cart’ trên sản phẩm thì chúng ta sẽ được trỏ về trang /line_items?id=<id>, chúng ta sửa lại phương thức create trong lớp LineItemsController để hàm này trỏ lại trang '/' như sau:

class LineItemsController < ApplicationController
    .
    .
    .
    # POST /line_items
    # POST /line_items.json
    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('/', :notice => 'Line item was successfully created') } 
                format.json { render :show, status: :created, location: @line_item }
            else
                format.html { render :new }
                format.json { render json: @line_item.errors, status: :unprocessable_entity }
            end
        end
    end
    .
    .
    .
end

Vậy là xong, bây giờ website của chúng ta sẽ có giao diện như thế này:

bbbbcapture

Tuy nhiên còn một vấn dề nhỏ nữa, nếu bạn trỏ vào các trang như /carts, /products, /line_items thì bạn sẽ bị lỗi biến @cart dùng để gọi render bên sidebar không tồn tại (có giá trị nil).

capture

Lý do là vì chúng ta chỉ định nghĩa biến này ở hàm index trong lớp StoreController thôi, để sửa thì bạn có thể vào từng phương thức index của từng lớp CartsController, ProductsControllerLineItemsController và chèn thêm dòng khai báo @cart = current_cart là sẽ hết lỗi, tuy nhiên cách này rất “cùi bắp” vì cứ mỗi lần tạo thêm controller thì chúng ta lại phải thêm một dòng như thế.

Do đó có một cách khác là chúng ta khai báo phương thức current_cart là một phương thức helper, tức là phương thức này sẽ có thể gọi được từ các file View, để làm điều này thì chúng ta thêm dòng khai báo sau phương thức current_cart như sau:

class ApplicationController < ActionController::Base
    # Prevent CSRF attacks by raising an exception.
    # For APIs, you may want to use :null_session instead.
    protect_from_forgery with: :exception
 
    private
 
    def current_cart
        Cart.find(session[:cart_id])
    rescue
        cart = Cart.create
        session[:cart_id] = cart.id
        cart
    end
 
    helper_method :current_cart
end

Phương thức helper_method nhận vào tên các phương thức (cách nhau bởi dấu phẩy), phương thức nào được truyền vào thì sẽ được định nghĩa là một phương thức helper.

Và chúng ta có thể gọi đến phương thức này trong file View, chúng ta sửa lại file application.html.erb như sau:

<!DOCTYPE html>
<html>
<head>
<title>Books Store</title>
    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
    <%= csrf_meta_tags %>
</head>
<body id="store">
    <div id="banner">
        <%= image_tag("logo.png") %>
        <%= @page_title || "Books Store" %>
    </div>
    <div id="columns">
        <div id="side">
            <div id="cart">
                <%= render current_cart %>
            </div>

            <a href="#">Home</a>
            <a href="#">FAQ</a>
            <a href="#">News</a>
            <a href="#">Contact</a>
        </div>
        <div id="main">
            <%= yield %>
        </div>
    </div>
</body>
</html>

Vậy là hết lỗi! Dòng @cart = current_cart trong phương thức index của lớp StoreController bây giờ trở nên vô nghĩa, bạn có thể bỏ đi nếu muốn.