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:
Để 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.
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:
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.
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.
Ở đâ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/carts…v.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:
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:
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.
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 địnhthư 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’.
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:
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ư:
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, after và around, 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.
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à sessions và admin.
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, create và destroy. 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:
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 id và name.
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.
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 modelrồ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_password và salt.
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.
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 là phương thức tạo nhanh Getter và Setter 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:
Hàm encrypt_password sẽ gọi hàm Digest::SHA2.hexdigest và nhận vào tham số là password và salt. 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 name và password, 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, create và user_params.
Hai phương thức update và create 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ố password và password_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, password và password_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_password và salt, 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ố password và password_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:
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 Turnon để 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.rb và production.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:
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à to và subject 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 đó.
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:
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: