Category Archives: Ruby

Rails – Initializers

Trong một project Rails có một thư mục tên là initializers nằm trong thư mục config, thư mục này dùng để chứa các file .rb, ý nghĩa của thư mục này là khởi tạo tài nguyên. Mỗi khi chúng ta chạy server, Rails load các file và module xong thì sẽ chạy các file code .rb được đặt trong thư mục này.

Chúng ta sẽ quy định project sử dụng ngôn ngữ tiếng Anh mặc định khi chạy project.

Chúng ta tạo một file có tên i18n.rb trong thư mục initializers như sau:

I18n.default_locale = :en

LANGUAGES = [
    ['English', 'en'],
    ['Tiếng Việt', 'vi']
]

Để thiết lập ngôn ngữ mặc định cho I18n thì chúng ta gán vào thuộc tính default_locale.

Ngoài ra ở đây chúng ta còn định nghĩa một mảng có tên là LANGUAGES chứa các phần tử là mảng khác, các mảng phần tử chứa tên các ngôn ngữ. Chúng ta tạo mảng này để tạo nút chọn ngôn ngữ cho người dùng sử dụng.

Bây giờ chúng ta sửa lại file layout 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"> 
        <%= form_tag '/', :class => 'locale', :method => :get do %>
            <%= select_tag 'set_locale',
                options_for_select(LANGUAGES, I18n.locale.to_s),
                :onchange => 'this.form.submit()' %>         
        <% end %>
        <%= image_tag("logo.png") %>
        <%= @page_title || t('.title') %>
    </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="#"><%= t('.home') %></a><br />
            <a href="#"><%= t('.faq') %></a><br />
            <a href="#"><%= t('.news') %></a><br />
            <a href="#"><%= t('.contact') %></a><br />
 
            <% if session[:user_id] %>
                <br/>
                <%= link_to 'Orders', '/orders' %><br />
                <%= link_to 'Products', '/products' %><br />
                <%= link_to 'Users', '/users' %><br />
                <%= button_to 'Logout', '/logout', :method => :delete %>
                <br/>
            <% else %>
                <br/>
                <%= link_to 'Log In', login_path %><br />
            <% end %>
        </div>
        <div id="main">
            <%= yield %>
        </div>
    </div> 
</body>
</html>

Chúng ta tạo một form bằng phương thức form_tag, form này sẽ gửi về trang '/' với phương thức là GET, bên trong chúng ta tạo một nút combobox bằng phương thức select_tag, phương thức này sẽ tạo thẻ <select> trong HTML.

Tham số đầu tiên của phương thức này là 'set_locale', đây là tên tham số được truyền lên server, Tham số thứ 2 là phương thức options_for_select(LANGUAGES, I18n.locale.to_s), có nghĩa là lấy danh sách các phần tử cho trong biến LANGGUAGES (đã được định nghĩa ở trên) để làm các item, giá trị được select mặc định lấy trong từ I18n.locale.to_s. Tham số thứ 3 là :onchange, tức là tên phương thức được gọi khi người dùng chọn một item khác, ở đây phương thức đó là this.form.submit(), tức là sẽ gửi form về server.

Form này chúng ta định nghĩa class riêng là locale, bây giờ chúng ta định nghĩa class CSS đó như sau:

.
.
.
/* Language chooser style */
.locale {
    float: right;
    margin: -0.25em 0.1em;
}

Cuối cùng chúng ta sửa phương thức index trong lớp StoreController như sau:

class StoreController < ApplicationController
    skip_before_filter :authorize
    
    def index 
        if params[:set_locale]
            @redirectURL = '/' + params[:set_locale] 
            redirect_to @redirectURL
        else
            @products = Product.all
            @cart = current_cart
        end
    end
end

Chúng ta kiểm tra xem lệnh gửi lên có kèm theo tham số set_locale hay không, nếu có thì chúng ta redirect về lại trang này với tham số ngôn ngữ đi kèm.

Rails – Đa ngôn ngữ – Phần 3

Chúng ta sẽ dịch lỗi trên model.

Đầu tiên chúng ta “dịch” form nhập thông tin thanh toán, trước tiên là file new.html.erb trong thư mục app/views/orders:

<div class="depot_form">
    <fieldset>
        <legend><%= t('.legend') %></legend>
            <%= render 'form' %> 
    </fieldset>
</div>

Tiếp theo là file _form.html.erb:

<%= 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 t('.name') %><br>
        <%= f.text_field :name, :size => 40 %>
    </div>
    <div class="field">
        <%= f.label :address, t('.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 t('.pay_type') %><br>
        <%= f.select :pay_type, Order::PAYMENT_TYPES, :prompt => t('.pay_type_combo') %>
    </div>
 
    <div class="actions">
        <%= f.submit t('.submit') %>
    </div>
<% end %>

Kế tiếp là file ngôn ngữ:

English:

en:
  .
  .
  .
  orders:
    new:
      legend: "Enter your information"
    form:
      name: "Name"
      address: "Address"
      pay_type: "Payment Type" 
      pay_type_combo: "Select a payment method"
      submit: "Checkout" 

Tiếng Việt:

vi:
  .
  .
  .
  orders:
    new:
      legend: "Thông tin thanh toán"
    form:
      name: "Tên"
      address: "Địa chỉ"
      pay_type: "Thanh toán" 
      pay_type_combo: "Chọn phương thức thanh toán"
      submit: "Đặt hàng" 

Bây giờ chúng ta có thể đặt hàng với giao diện tiếng Việt, và khi bấm nút “Đặt hàng” mà không điền cái gì vào thì chúng ta sẽ được trang thông báo lỗi như sau:

Do đó bây giờ chúng ta cần khai báo bản dịch cho phần báo lỗi.

Để dịch thì chúng ta sẽ dịch trên đối tượng ActiveRecord chứ không phải là các chuỗi do chúng ta tự khai báo trong view nữa, do đó chúng ta phải khai báo các khóa do Rails quy định.

Chúng ta chỉ cần chỉnh sửa phần tiếng Việt thôi, không cần khai báo phần ngôn ngữ tiếng Anh. Chúng ta sửa lại file vi.yml như sau:

vi:
  .
  .
  .
  activerecord: 
    models:
      order: "Đơn hàng" 
    attributes:
      order:
        name: "Tên" 
        address: "Địa chỉ" 
        email: "Email"
        pay_type: "Phương thức thanh toán" 
    errors: 
      messages:
        blank: "không được để trống" 
        inclusion: "không thuộc danh sách cho trước"  
      template: 
        body: "Lỗi xảy ra ở các trường sau:" 
      header:
        one: "1 lỗi xảy ra" 
        other: "%{count} lỗi xảy ra" 

Để dịch các tên model thì chúng ta truyền theo cấu trúc activerecord → models → <tên_model>. 

Dịch tên các thuộc tính/trường thì theo cấu trúc activerecord  → attributes  <tên_model>  <tên_thuộc_tính>

Dịch các dòng báo lỗi thì theo cấu trúc: activerecord → errors → messages → <tên_loại_lỗi>. Ở đây tên loại lỗi là blank, tức là lỗi thuộc tính không được để trống, inclusion là thuộc tính không thuộc một tập giá trị cho trước. Ngoài ra còn có các lỗi như confirmation, accepted, present…v.v Bạn có thể xem tại đây.

Ngoài ra còn có một số khóa khác như template, header, body, one, other. Trong đó one là hiển thị câu thông báo có 1 lỗi và other là thông báo có nhiều lỗi. Lý do phải chia ra 2 loại là vì có một số ngôn ngữ hiển thị số ít và số nhiều khác nhau, như tiếng Anh thì phải thêm kí tự ‘s’ sau cùng. Ở đây khóa other nhận vào giá trị từ biến count, chúng ta phải truyền biến này vào khi gọi hàm t().

Do đó bây giờ chúng ta sửa lại file _form.html.erb như sau:

<%= form_for(@order) do |f| %>
    <% if @order.errors.any? %>
        <div id="error_explanation">
            <h2><%= t('activerecord.errors.template.header', 
                    :count => @order.errors.size) %>.</h2> 
                <p><%= t('activerecord.errors.template.body') %></p>
            <ul>
                <% @order.errors.full_messages.each do |message| %>
                <li><%= message %></li>
                <% end %>
            </ul>
        </div>
    <% end %>
    .
    .
    .
<% end %>

Vậy là các câu thông báo lỗi đã được dịch xong:

Chúng ta dịch thêm một thứ nữa là câu thông báo đặt hàng thành công:

Đầu tiên 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: I18n.t('.thanks') }
                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

Ở đây chúng ta phải ghi rõ ra phương thức t() của module I18n.

Tiếp theo là các file ngôn ngữ:

English:

en:
  .
  .
  .
  thanks: "Thank you for your order"

Tiếng Việt:

vi:
  .
  .
  .
  thanks: "Cám ơn bạn đã đặt hàng"

Vậy là xong!

Rails – Đa ngôn ngữ – Phần 2

Trong phần này chúng ta sẽ “dịch” một số chuỗi ra ngôn ngữ tiếng Việt.

Bây giờ chúng ta sửa lại file layout chính 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 || t('.title') %>
    </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="#"><%= t('.home') %></a><br />
            <a href="#"><%= t('.faq') %></a><br />
            <a href="#"><%= t('.news') %></a><br />
            <a href="#"><%= t('.contact') %></a><br />
 
            <% if session[:user_id] %>
                <br/>
                <%= link_to 'Orders', '/orders' %><br />
                <%= link_to 'Products', '/products' %><br />
                <%= link_to 'Users', '/users' %><br />
                <%= button_to 'Logout', '/logout', :method => :delete %>
                <br/>
            <% else %>
                <br/>
                <%= link_to 'Log In', login_path %><br />
            <% end %>
        </div>
        <div id="main">
            <%= yield %>
        </div>
    </div> 
</body>
</html>

Ở đây chúng ta gọi hàm t() thay vì ghi các chuỗi cụ thể ra luôn. Hàm t() là hàm của module i18n, đây là tên viết tắt của hàm translate(), hàm này nhận vào tên khóa và lấy giá trị tương ứng trong file .yml ra để hiển thị.

 

Bây giờ chúng ta phải tạo các khóa đó, chúng ta có thể đặt các file .yml trong cùng thư mục với từng View, tức là các thư mục views/admin, views/cartsv.v hoặc đặt trong thư mục mặc định của Rails là config/locales, ở đây chúng ta sẽ dùng thư mục mặc định cho dễ tìm, chúng ta sửa lại file en.yml trong thư mục config/locales như sau:

en: 
  layouts:
    application:
      home: "Home" 
      title: "Books Store"
      faq: "FAQ" 
      news: "News" 
      contact: "Contact"

Chúng ta phải khai báo khóa layout:application: rồi mới tới các khóa kia.

Lưu ý là chúng ta dùng 2 kí tự cách để thụt đầu dòng chứ không dùng dấu Tab. Đây là cú pháp của ngôn ngữ YAML (.yml).

Với tiếng Việt thì chúng ta cũng làm tương tự là tạo file vi.yml trong thư mục config/locales như sau:

vi:
  layouts:
    application:
      home: "Trang chủ" 
      title: "Books Store"
      faq: "Hỏi đáp" 
      news: "Tin tức" 
      contact: "Liên hệ"

Bây giờ chúng ta có thể dùng URL http://localhost:3000/vi được rồi.

Bây giờ chúng ta sẽ “dịch” trang store/index và trang hiển thị giỏ hàng. Đầu tiên chúng ta định nghĩa các đoạn text như sau:

English:

en: 
  layouts:
    application: 
      home: "Home" 
      title: "Books Store"
      faq: "FAQ" 
      news: "News" 
      contact: "Contact"
 
  store:
    index:
      title: "Products List" 
      add: "Add to Cart"
  carts:
    cart:
      title: "Your cart" 
      empty_cart: "Empty cart" 
      checkout: "Checkout"

Tiếng Việt:

vi:
  layouts:
    application:
      home: "Trang chủ" 
      title: "Books Store"
      faq: "Hỏi đáp" 
      news: "Tin tức" 
      contact: "Liên hệ"

  store:
    index:
      title: "Sản phẩm"
      add: "Thêm vào giỏ hàng"
 
  carts:
    cart:
      title: "Giỏ hàng"
      empty_cart: "Xóa giỏ hàng"
      checkout: "Thanh toán"

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

<% if notice %>
    <p id="notice"><%= notice %></p>
<% end %>
 
<h1><%= t('.title') %></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 t('.add'), line_items_path(:product_id => product),
                :remote => true %>
        </div>
    </div>
<% end %>

Kế tiếp là file _cart.html.erb trong thư mục app/views/carts:

<h2><%= t('.title') %></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 t('.checkout'), new_order_path, :method => :get %><br>
<%= button_to t('.empty_cart'), 
    cart, 
    :method => :delete,
    data: {:confirm => 'Are you sure?' } %>

Bây giờ chúng ta sẽ “dịch” chuỗi hiển thị tiền từ USD sang VNĐ. Ở đây chúng ta chỉ hiển thị cho khác đi thôi chứ không chuyển đổi tỉ giá. Nếu muốn bạn có thể tự chuyển.

Để chuyển đổi tiền tệ thì chúng ta khai báo trong file .yml như sau:

English:

en:
  .
  .
  .
  number:
    currency:
      format:
        unit: "$"
        precision: 2
        separator: "."
        delimiter: ","
        format: "%u%n" 

Một số lưu ý như format là %u%n, thì trong đó %u là hiển thị kí tự tiền tệ, %n là số tiền, precision là số lượng chữ số sau phần thập phân, separator là kí tự ngăn cách giữa phần thập phân và phần đơn vị, delimiter là kí tự ngăn cách phần ngàn.

Tiếng Việt:

vi:
  .
  .
  .
  number:
    currency:
      format:
        unit: đồng
        precision: 3
        separator: ","
        delimiter: "."
        format: "%n %u"

Hàm number_to_currency() sẽ tự động tìm các giá trị trong file .yml để hiển thị cho đúng.

Rails – Đa ngôn ngữ – Phần 1

Chúng ta sẽ dùng I18n để hiển thị website trên nhiều ngôn ngữ khác nhau.

Đa ngôn ngữ là một bài toán khó, do đó chúng ta sẽ không thực hiện bằng cách dịch bình thường, mà thay vào đó là các chuỗi text cố định sẽ được hiển thị khác nhau với từng ngôn ngữ khác nhau.

Mặc định thì các ứng dụng Rails đều sử dụng I18n rồi, và ngôn ngữ duy nhất được sử dụng là tiếng Anh, các file ngôn ngữ sẽ được đặt trong thư mục config/locale với đuôi .yml, mặc định thư mục này chứa 1 file là en.yml:

# Files in the config/locales directory are used for internationalization
# and are automatically loaded by Rails. If you want to use locales other
# than English, add the necessary files in this directory.
#
# To use the locales, use `I18n.t`:
#
# I18n.t 'hello'
#
# In views, this is aliased to just `t`:
#
# <%= t('hello') %>
#
# To use a different locale, set it with `I18n.locale`:
#
# I18n.locale = :es
#
# This would use the information in config/locales/es.yml.
#
# To learn more, please read the Rails Internationalization guide
# available at http://guides.rubyonrails.org/i18n.html.

en:
 hello: "Hello world"

Chúng ta sẽ làm việc với các file này sau.

Bây giờ chúng ta sẽ thêm chức năng hỗ trợ URL đa ngôn ngữ.

Chúng ta sẽ đưa tham số ngôn ngữ vào sau đuôi URL, ví dụ http://localhost:3000/en/products.

Đầu tiên chúng ta sửa lại file routes.rb trong thư mục config như sau:

Rails.application.routes.draw do
    get 'admin/index'
    get 'sessions/new'
    get 'sessions/create'
    get 'sessions/destroy' 
    get 'store/index' 
 
    get 'admin' => 'admin#index'
 
    controller :sessions do
        get 'login' => :new
        post 'login' => :create
        delete 'logout' => :destroy
    end
 
    scope '(:locale)' do
        resources :users
        resources :orders
        resources :line_items
        resources :carts
        resources :products do
            get :who_bought, :on => :member
        end
        root :to => 'store#index', :as => 'store'
    end
end

Chúng ta gom các đoạn routing cho model – tức là các phương thức resources vào trong phương thức scope.

Phương thức scope ':locale' sẽ nối chuỗi trong biến :locale vào trước URL, tức là /products thì sẽ là en/products, ở đây chúng ta bọc :locale trong cặp dấu ngoặc tròn (), tức là có thể dùng hoặc không dùng cũng được.

Tiếp theo chúng ta cần khai báo biến :locale đó, chúng ta sửa lại lớp ApplicationController như sau:

class ApplicationController < ActionController::Base
    # Prevent CSRF attacks by raising an exception.
    # For APIs, you may want to use :null_session instead.
    before_filter :authorize
    before_filter :set_i18n_locale
 
    protect_from_forgery with: :exception
 
  private
 
    def current_cart
        Cart.find(session[:cart_id])
    rescue ActiveRecord::RecordNotFound
        cart = Cart.create
        session[:cart_id] = cart.id
        cart
    end
 
    helper_method :current_cart
 
  protected
 
    def authorize
        @user = User.find_by_id(session[:user_id]) 
        if @user == nil
            redirect_to '/login', :notice => 'You must login first'
        end
    end
 
    def set_i18n_locale 
        if params[:locale] 
            if I18n.available_locales.include?(params[:locale].to_sym)
                I18n.locale = params[:locale] 
            else
                flash.now[:notice] = params[:locale] + ' is not supported'               
            end
        end
    end
 
    def default_url_options 
        { :locale => I18n.locale }
    end
end

Phương thức set_i18n_locale sẽ được dùng trong phương thức before_filter, ở đây phương thức này sẽ kiểm tra xem trong URL gửi lên có tham số nào là :locale hay không, nếu có thì kiểm tra xem tham số đó có trong danh sách ngôn ngữ của I18n không, nếu có thì gán giá trị của tham số đó vào thuộc tính I18n.locale, không thì hiển thị lỗi.

Phương thức default_url_options là phương thức có sẵn của lớp ActionController::Base, phương thức này làm nhiệm vụ thiết lập các tham số trong lệnh HTTP được gửi lên. Chúng ta override phương thức này, ở đây chúng ta khai báo biến :locale có giá trị là giá trị của thuộc tính I18n.locale, tham số này sẽ được dùng trong phương thức helper là url_for.

Chúng ta sẽ tiếp tục làm việc với các phương thức này sau. Bây giờ nếu chúng ta trỏ đến URL http://localhost:3000/en/products thì sẽ không có gì khác biệt, còn nếu truyền vào một giá trị khác như http://localhost:3000/vi/products thì sẽ có dòng thông báo như ‘vi is not supported’.

Rails – Callback

Trong quá trình chạy ứng dụng, một đối tượng có thể được tạo ra, được cập nhật hoặc bị hủy thông qua các thao tác CRUD. Rails cung cấp cơ chế callback để chúng ta có thể kiểm soát trạng thái của các đối tượng này.

Callback là các phương thức/hàm được gọi trước hoặc sau khi có sự thay đổi trạng thái (như tạo, lưu, xóa, cập nhật, validate…) của đối tượng.

Ví dụ

Chúng ta sẽ không cho thực hiện chức năng xóa user nếu trong bảng chỉ còn lại một user.

Đầu tiên chúng ta sửa lại file layout một tí 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 />
 
            <% if session[:user_id] %>
                <br/>
                <%= link_to 'Orders', '/orders' %><br />
                <%= link_to 'Products', '/products' %><br />
                <%= link_to 'Users', '/users' %><br />
                <%= button_to 'Logout', '/logout', :method => :delete %>
                <br/>
            <% else %>
                <br/>
                <%= link_to 'Log In', login_path %><br />
            <% end %>
        </div>
        <div id="main">
            <%= yield %>
        </div>
    </div> 
</body>
</html>

Chúng ta tạo thêm mấy nút bấm dẫn đến trang /products, /users, /orders, /logout nếu người dùng đã đăng nhập, nếu chưa thì hiển thị nút dẫn đến trang /login.

Tiếp theo chúng ta sửa lại lớp User 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) 
            puts encrypt_password(password, user.salt)
            if user.hashed_password == encrypt_password(password, user.salt) 
                user
            end
        end
    end
 
    after_destroy :check_user_empty
 
    def check_user_empty
        if User.count.zero?
            raise "Can't delete last user"
        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

Chúng ta định nghĩa phương thức check_user_empty, phương thức này kiểm tra xem trong bảng User có rỗng hay không, nếu rỗng thì giải phóng một lỗi exception.

Sau đó ở trên chúng ta gọi phương thức after_destroy :check_user_empty. Phương thức after_destroy là một phương thức callback, phương thức này sẽ gọi phương thức :check_user_empty mỗi khi có một thao tác nào đó liên quan đến câu lệnh DELETE trong cơ sở dữ liệu xảy ra. Tức là ở đây nếu người dùng bấm nút ‘Destroy’ để xóa user thì phương thức check_user_exist sẽ được gọi, và nếu bảng users trong CSDL không còn bản ghi nào thì một lỗi exception sẽ được sinh ra.

Và nếu sau lời gọi hàm callback mà có lỗi exception nào đó thì lỗi này sẽ được gửi ngược về nơi đã gọi ra nó, tức là ở đây tương ứng với lời gọi @user.destroy trong phương thức destroy của lớp UsersController. Ngoài ra lỗi exception cũng sẽ bắt buộc Rails phải “đảo ngược” câu truy vấn vừa thực hiện, tức là nếu xóa user mà có lỗi exception đó thì user đó sẽ được phục hồi nguyên vẹn trong CSDL.

Bây giờ chúng ta sửa lại phương thức destroy trong lớp UsersController để bắt exception như sau:

class UsersController < ApplicationController
    .
    .
    .
    # DELETE /users/1
    # DELETE /users/1.json
    def destroy 
        begin
            @user.destroy
            flash[:notice] = "User #{@user.name} deleted"
        rescue Exception => e
            flash[:notice] = e.message
        end
        respond_to do |format|
            format.html { redirect_to users_url }
            format.json { head :no_content }
        end
    end
    .
    .
    .
end

Nếu có lỗi exception xảy ra thì chúng ta chỉ đơn giản là thêm câu thông báo vào biến flash.

Nếu bạn muốn Rails chỉ thực hiện phục hồi dữ liệu chứ không muốn tạo một đối tượng exception nào thì trong lớp User, chúng ta cho giải phóng một đối tượng ActiveRecord::Rollback là được.

Ngoài ra còn có rất nhiều phương thức callback khác như:

CREATE

  • before_validation
  • after_validation
  • before_save
  • around_save
  • before_create
  • around_create
  • after_create
  • after_save
  • after_commit/after_rollback

UPDATE

  • before_validation
  • after_validation
  • before_save
  • around_save
  • before_update
  • around_update
  • after_update
  • after_save
  • after_commit/after_rollback

DELETE

  • before_destroy
  • around_destroy
  • after_destroy
  • after_commit/after_rollback

Bạn có thể tìm hiểu thêm tại đây.

Rails – Filter

Filter là các phương thức chạy trước, sau hoặc cùng với một phương thức action (phương thức của controller).

Các phương thức filter có tính thừa kế, tức là nếu chúng ta gọi các phương thức filter trong lớp ApplicationController, thì các lớp kế thừa nó cũng chạy các phương thức filter đó.

Trong phần này chúng ta sẽ sử dụng filter để chặn truy cập vào trang /admin/index nếu người dùng chưa đăng nhập.

Đầu tiên chúng ta sửa lại lớp ApplicationController như sau:

class ApplicationController < ActionController::Base
    # Prevent CSRF attacks by raising an exception.
    # For APIs, you may want to use :null_session instead.
    before_filter :authorize
 
    protect_from_forgery with: :exception
 
  private
   
    def current_cart
        Cart.find(session[:cart_id])
            rescue ActiveRecord::RecordNotFound
            cart = Cart.create
            session[:cart_id] = cart.id
        cart
    end
 
    helper_method :current_cart
 
  protected
 
    def authorize
        @user = User.find_by_id(session[:user_id]) 
        if @user == nil
            redirect_to '/login', :notice => 'You must login first'
        end
    end
end

Filter có 3 loại là before, afteraround, tương ứng với chạy trước, sau hoặc chạy cùng.

Ở đây chúng ta dùng phương thức before_filter, và truyền vào tham số :authorize, đây là phương thức kiểm tra xem người dùng có đăng nhập hay chưa do chúng ta tự định nghĩa. Nếu người dùng chưa đăng nhập thì chúng ta cho trỏ tới trang /login.

Phương thức authorize sẽ chạy trước các phương thức action khác, tuy nhiên có một vấn đề, như đã nói ở trên là các filter sẽ được thừa kế, tức là các lớp controller con cũng sẽ chạy dòng before_filter ở trên. Trong số các controller kế thừa có cả controller chịu trách nhiệm việc hiển thị trang /login, tức là ở đây chúng ta không cho người dùng đăng nhập luôn.

Để giải quyết việc này thì chúng ta dùng các phương thức skip, ví dụ chúng ta thêm dòng này vào đầu lớp SessionsController như sau:

class SessionsController < ApplicationController
    skip_before_filter :authorize
    .
    .
    .
end

Có thể hiểu phương thức skip_before_filter :authorize sẽ đưa phương thức authorize vào danh sách “đen”, tức là không được chạy. Và do đó chúng ta có thể chạy các phương thức khác bình thường.

Và không chỉ có lớp SessionsController mà tất cả các lớp controller con khác cũng cần được “mở khóa” nữa. Chúng ta lần lượt sửa lại như sau:

Lớp StoreController:

class StoreController < ApplicationController
    skip_before_filter :authorize
    .
    .
    .
end

Lớp CartsController:

class CartsController < ApplicationController
    skip_before_filter :authorize, :only => [:create, :update, :destroy]
    .
    .
    .
end

Chúng ta có thể truyền vào tham số :only, tham số này sẽ quy định chỉ có một số phương thức nhất định được “mở khóa”.

Lớp LineItemsController:

class LineItemsController < ApplicationController
    skip_before_filter :authorize, :only => :create
    .
    .
    .
end

Lớp OrdersController:

class OrdersController < ApplicationController
    skip_before_filter :authorize, :only => [:new, :create]
    .
    .
    .
end

Vậy là xong, bây giờ nếu chúng ta vào trang /admin/index hay một số trang nhất định mà chưa đăng nhập thì chúng ta sẽ được trỏ về trang /login.

Bạn có thể tìm hiểu thêm về các phương thức filter tại đây.

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