[Ruby]《Ruby on Rails Tutorial》的搬
背景:
- 最近比较闲,想学习ruby on rails
- 于是找到了https://www.railstutorial.org 上的首推教程《Ruby on Rails Tutorial》
屏幕快照 2016-05-29 上午11.04.20.png
这本书第一章和第二章讲了2个基本demo,实在没啥意思,姑且略过. 从第三章开始到第十二章是从0到1实现了一个类似Twitter的简单社交网站(首页,登录注册,发布推文,关注等功能). 怎么样是不是很棒?
但是这个本书实在讲得过于详细,对于我这种本身没有那么多时间(也没那么多耐心😢)去一点一点看下来的童鞋,看着实在太着急了,于是准备快速整理下(把里面的干货和代码提取出来),方便大家可以分分钟coding出这个demo出来.
当然真正学习还是要看原教程,我这个只是"扒皮版本".
<br />
原文链接
RUBY ON RAILS TUTORIAL
https://www.railstutorial.org/book/static_pages
他们的github:
railstutorial/sample_app_rails_4
https://github.com/railstutorial/sample_app_rails_4
<br />
ruby学习框架图
ruby on rails is hard?第3-7章节见:
[Ruby]RUBY ON RAILS TUTORIAL 的搬运工第一天
<br />
下面是第8章开始
<br />
8. Log in, log out
8.1 Sessions
8.1.1 Sessions controller
生成session controller
rails generate controller Sessions new
修改config
vim config/routes.rb
Rails.application.routes.draw do
root 'static_pages#home'
get 'help' => 'static_pages#help'
get 'about' => 'static_pages#about'
get 'contact' => 'static_pages#contact'
get 'signup' => 'users#new'
get 'login' => 'sessions#new'
post 'login' => 'sessions#create'
delete 'logout' => 'sessions#destroy'
resources :users
...
8.1.2 Login form
登录的表单:
vim app/views/sessions/new.html.erb
<% provide(:title, "Log in") %>
<h1>Log in</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(:session, url: login_path) do |f| %>
<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.submit "Log in", class: "btn btn-primary" %>
<% end %>
<p>New user? <%= link_to "Sign up now!", signup_path %></p>
</div>
</div>
8.1.3 Finding and authenticating a user
寻找/鉴权 一个用户:
vim app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
# Log the user in and redirect to the user's show page.
else
# Create an error message.
render 'new'
end
end
def destroy
end
end
8.1.4 Rendering with a flash message
动画方法了:
vim app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
# Log the user in and redirect to the user's show page.
else
flash[:danger] = 'Invalid email/password combination' # Not quite right!
render 'new'
end
end
def destroy
end
end
8.2 Logging in
登录
vim app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
include SessionsHelper
end
8.2.1 The log_in method
首先我们写个login方法:
vim app/helpers/sessions_helper.rb
module SessionsHelper
# Logs in the given user.
def log_in(user)
session[:user_id] = user.id
end
end
然后,SessionsController中调用这个方法:
vim app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
log_in user
redirect_to user
else
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end
def destroy
end
end
8.2.2 Current user
用户一旦登录后,我们需要一个方法能获取当前登录的该用户:
polen:
这里补充个知识点:
||= 这个最直观的意思是a||=b, 就是a= a||b ,类似如下:
x = x + 1 -> x += 1
x = x * 3 -> x *= 3
x = x - 8 -> x -= 8
x = x / 2 -> x /= 2
@foo = @foo || "bar" -> @foo ||= "bar"
但这个问题如果深究,其实二者并不是完全等于的,区别在于a是否定义,所以严格解释是:
a||=b: if defined?(a) then (a || a = b) else a = b end
[参考]:
Ruby'陷阱'之: '||=' 的真正展开式
What Ruby’s ||= (Double Pipe / Or Equals) Really Does
然后来说我们的current_user,(就用到了上面所说的||=):
vim app/helpers/sessions_helper.rb
module SessionsHelper
# Logs in the given user.
def log_in(user)
session[:user_id] = user.id
end
# Returns the current logged-in user (if any).
def current_user
@current_user ||= User.find_by(id: session[:user_id])
end
end
8.2.3 Changing the layout links
检查是否login:
vim app/helpers/sessions_helper.rb
module SessionsHelper
# Logs in the given user.
def log_in(user)
session[:user_id] = user.id
end
# Returns the current logged-in user (if any).
def current_user
@current_user ||= User.find_by(id: session[:user_id])
end
# Returns true if the user is logged in, false otherwise.
def logged_in?
!current_user.nil?
end
end
如果是登录用户,那么顶部菜单栏的布局就需要修改一下,根据<% if logged_in? %>
决定显示的布局区分:
vim app/views/layouts/_header.html.erb
<header class="navbar navbar-fixed-top navbar-inverse">
<div class="container">
<%= link_to "sample app", root_path, id: "logo" %>
<nav>
<ul class="nav navbar-nav navbar-right">
<li><%= link_to "Home", root_path %></li>
<li><%= link_to "Help", help_path %></li>
<% if logged_in? %>
<li><%= link_to "Users", '#' %></li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
Account <b class="caret"></b>
</a>
<ul class="dropdown-menu">
<li><%= link_to "Profile", current_user %></li>
<li><%= link_to "Settings", '#' %></li>
<li class="divider"></li>
<li>
<%= link_to "Log out", logout_path, method: "delete" %>
</li>
</ul>
</li>
<% else %>
<li><%= link_to "Log in", login_path %></li>
<% end %>
</ul>
</nav>
</div>
</header>
这里的profile使用了下拉菜单的模式,所以需要include bootstrap自定义的javascript:
vim app/assets/javascripts/application.js
//= require jquery
//= require jquery_ujs
//= require bootstrap
//= require turbolinks
//= require_tree .
8.2.5 Login upon signup
做一个注册后自动登录
vim app/controllers/users_controller.rb
class UsersController < ApplicationController
def show
@user = User.find(params[:id])
end
def new
@user = User.new
end
def create
@user = User.new(user_params)
if @user.save
log_in @user
flash[:success] = "Welcome to the Sample App!"
redirect_to @user
else
render 'new'
end
end
private
def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end
end
8.3 Logging out
退出操作,首先写一个退出方法:
vim app/helpers/sessions_helper.rb
...
# Logs out the current user.
def log_out
session.delete(:user_id)
@current_user = nil
end
使用退出方法:
vim app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
...
...
def destroy
log_out
redirect_to root_url
end
8.4 Remember me
用户数据的存储问题会在这一篇中解决:
8.4.1 Remember token and digest
a). 我们需要在users这个model里加入remember_digest
屏幕快照 2016-05-30 下午2.52.56.png
rails generate migration add_remember_digest_to_users remember_digest:string
rake db:migrate
b). 然后我们需要一个生成token的方法以及remember方法:
vim app/model/user.rb
class User < ActiveRecord::Base
attr_accessor :remember_token
before_save { self.email = email.downcase }
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
has_secure_password
validates :password, presence: true, length: { minimum: 6 }
# Returns the hash digest of the given string.
def User.digest(string)
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
BCrypt::Engine.cost
BCrypt::Password.create(string, cost: cost)
end
# Returns a random token.
def User.new_token
SecureRandom.urlsafe_base64
end
# Remembers a user in the database for use in persistent sessions.
def remember
self.remember_token = User.new_token
update_attribute(:remember_digest, User.digest(remember_token))
end
end
8.4.2 Login with remembering
a). user model中加入一个校验方法:
vim app/models/user.rb
class User < ActiveRecord::Base
...
# Returns true if the given token matches the digest.
def authenticated?(remember_token)
BCrypt::Password.new(remember_digest).is_password?(remember_token)
end
...
b). 登陆后记录这个用户
vim app/controllers/sessions_controller.rb
...
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
log_in user
remember user
redirect_to user
else
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end
...
c). sessions 中也要更新current_user:
vim app/helpers/sessions_helper.rb
module SessionsHelper
...
# Remembers a user in a persistent session.
def remember(user)
user.remember
cookies.permanent.signed[:user_id] = user.id
cookies.permanent[:remember_token] = user.remember_token
end
# Returns the user corresponding to the remember token cookie.
def current_user
if (user_id = session[:user_id])
@current_user ||= User.find_by(id: user_id)
elsif (user_id = cookies.signed[:user_id])
user = User.find_by(id: user_id)
if user && user.authenticated?(cookies[:remember_token])
log_in user
@current_user = user
end
end
end
...
8.4.3 Forgetting users
用户登录的时候,需要remember记录,
对应:用户退出的时候,需要forget用户:
vim app/models/user.rb
class User < ActiveRecord::Base
...
# Forgets a user.
def forget
update_attribute(:remember_digest, nil)
end
...
从session中删除对应info:
vim app/helpers/sessions_helper.rb
...
# Forgets a persistent session.
def forget(user)
user.forget
cookies.delete(:user_id)
cookies.delete(:remember_token)
end
...
8.4.4 Two subtle bugs
既然做了登录登出,自然会有多个浏览器各自登录登出的问题,所以我们如何修复这类问题呢?
a). 首先log_out if logged_in?
vim app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
.
.
.
def destroy
log_out if logged_in?
redirect_to root_url
end
end
b). 如果用户已经退出,数据被清空了,那么authenticated也应该直接返回false
vim app/models/user.rb
class User < ActiveRecord::Base
...
# Returns true if the given token matches the digest.
def authenticated?(remember_token)
return false if remember_digest.nil?
BCrypt::Password.new(remember_digest).is_password?(remember_token)
end
...
8.4.5 “Remember me” checkbox
加个checkbox,如下:
“Remember me” checkbox
a). 直接修改new.html
vim app/views/sessions/new.html.erb
<% provide(:title, "Log in") %>
<h1>Log in</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(:session, url: login_path) do |f| %>
<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.label :remember_me, class: "checkbox inline" do %>
<%= f.check_box :remember_me %>
<span>Remember me on this computer</span>
<% end %>
<%= f.submit "Log in", class: "btn btn-primary" %>
<% end %>
<p>New user? <%= link_to "Sign up now!", signup_path %></p>
</div>
</div>
b). 对应的css布局也要调一下的咯:
vim app/assets/stylesheets/custom.css.scss
/* forms */
.
.
.checkbox {
margin-top: -10px;
margin-bottom: 10px;
span {
margin-left: 20px;
font-weight: normal;
}
}
#session_remember_me {
width: auto;
margin-left: 0;
}
c). UI完成了,之后是具体逻辑层的操作:
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
...
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
log_in user
params[:session][:remember_me] == '1' ? remember(user) : forget(user)
redirect_to user
else
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end
...
polen:
这里文章插了个小段子,这个世界上只有10中人,一种喜欢ternary operator(三元运算),另一种不喜欢.
就是使用:
boolean? ? do_one_thing : do_something_else
不使用:
if boolean?
do_one_thing
else
do_something_else
end
其实就是代码习惯的问题了,刚好前天看覃超的直播中怎样才能通过国外的程序员面试?
里面说到代码问题--一定要清晰,简洁,让人看得懂:
//推荐的代码习惯
return x>y;
>//不推荐的代码习惯:
if x>y
return true;
else
return false;
<br />
9. Updating, showing, and deleting users
对用户实现REST的操作:
9.1 Updating users
9.1.1 Edit form
a). controller中增加编辑功能
vim app/controllers/users_controller.rb
...
def edit
@user = User.find(params:[:id])
end
...
b). 开始画UI:
vim app/views/users/edit.html.erb
<% provide(:title, "Edit user") %>
<h1>Update your profile</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(@user) do |f| %>
<%= render 'shared/error_messages' %>
<%= f.label :name %>
<%= f.text_field :name, class: 'form-control' %>
<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.label :password_confirmation, "Confirmation" %>
<%= f.password_field :password_confirmation, class: 'form-control' %>
<%= f.submit "Save changes", class: "btn btn-primary" %>
<% end %>
<div class="gravatar_edit">
<%= gravatar_for @user %>
<a href="http://gravatar.com/emails" target="_blank">change</a>
</div>
</div>
</div>
c). 首页导航栏的setting可以完善url了
vim app/views/layouts/_header.html.erb
<header class="navbar navbar-fixed-top navbar-inverse">
...
<ul class="dropdown-menu">
<li><%= link_to "Profile", current_user %></li>
<li><%= link_to "Settings", edit_user_path(current_user) %></li>
<li class="divider"></li>
<li>
<%= link_to "Log out", logout_path, method: "delete" %>
</li>
</ul>
9.1.2 Unsuccessful edits
a). update的action:
vim app/controllers/users_controller.rb
class UsersController < ApplicationController
...
def update
@user = User.find(params[:id])
if @user.update_attributes(user_params)
flash[:success] = "Profile updated"
redirect_to @user
else
render 'edit'
end
end
...
b). 用户可以编辑之后,会有个小问题,之前因为限制了密码,这里需要允许密码为空
polen:
这里不会和注册需要密码为空冲突,因为注册的时候has_secure_password回确保密码不为空,这里只是UsersController 在update_attributes的时候确保可以正常更新
vim app/models/user.rb
class User < ActiveRecord::Base
...
validates :password, presence: true, length: { minimum: 6 }, allow_nil: true
...
9.2 Authorization
9.2.1 Requiring logged-in users
用户如果没有登录,有些界面就需要提醒他去登录界面,具体哪几个界面呢?目前仅仅是edit界面和update界面.
所以我们首先需要写一个logged_in_user方法,检查如果没登录就重定向过去. 然后在最前面加一个before_action,确保优先执行改方法,only限定执行的方法范围.
vim app/controllers/users_controller.rb
class UsersController < ApplicationController
before_action :logged_in_user, only: [:edit, :update]
...
private
def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end
# Before filters
# Confirms a logged-in user.
def logged_in_user
unless logged_in?
flash[:danger] = "Please log in."
redirect_to login_url
end
end
9.2.2 Requiring the right user
确保用户可以编辑的仅仅是自己的user info
a). 增加current_user? 这个方法(和之前的current_user不同)
vim app/helpers/sessions_helper.rb
module SessionsHelper
...
# Returns true if the given user is the current user.
def current_user?(user)
user == current_user
end
# Returns the user corresponding to the remember token cookie.
def current_user
if (user_id = session[:user_id])
@current_user ||= User.find_by(id: user_id)
elsif (user_id = cookies.signed[:user_id])
user = User.find_by(id: user_id)
if user && user.authenticated?(cookies[:remember_token])
log_in user
@current_user = user
end
end
end
...
b). controller中统一增加correct_user方法:
vim app/controllers/users_controller.rb
class UsersController < ApplicationController
before_action :logged_in_user, only: [:edit, :update]
before_action :correct_user, only: [:edit, :update]
...
...
# Confirms a logged-in user.
def logged_in_user
unless logged_in?
flash[:danger] = "Please log in."
redirect_to login_url
end
end
# Confirms the correct user.
def correct_user
@user = User.find(params[:id])
redirect_to(root_url) unless current_user?(@user)
end
9.2.3 Friendly forwarding
这里是个小的"人性化设计":
我们坚持登录的操作会引发一个小问题:
一个非登录用户,如果想去访问编辑页面,但是登录后会进入user/1而非user/1/edit, 所以人性化的考虑是,记住他之前的去向,在登陆后,继续去他想去的页面.
vim app/helpers/sessions_helper.rb
module SessionsHelper
...
# Redirects to stored location (or to the default).
def redirect_back_or(default)
redirect_to(session[:forwarding_url] || default)
session.delete(:forwarding_url)
end
# Stores the URL trying to be accessed.
def store_location
session[:forwarding_url] = request.url if request.get?
end
...
修改controller:
vim app/controllers/users_controller.rb
class UsersController < ApplicationController
...
# Confirms a logged-in user.
def logged_in_user
unless logged_in?
store_location
flash[:danger] = "Please log in."
redirect_to login_url
end
end
...
在create这里也需要改一改:
vim app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
...
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
log_in user
params[:session][:remember_me] == '1' ? remember(user) : forget(user)
redirect_back_or user
else
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end
...
9.3 Showing all users
接下来我们要做个用户列表,(常见APP的通讯录,好友列表,粉丝列表都会遇到...)
屏幕快照 2016-05-30 下午8.18.44.png
9.3.1 Users index
a). controller增加方法:
//app/controllers/users_controller.rb
class UsersController < ApplicationController
before_action :logged_in_user, only: [:edit, :update]
before_action :correct_user, only: [:edit, :update]
def index
@users = User.all
end
...
b). 画UI
//app/views/users/index.html.erb
<% provide(:title, 'All users') %>
<h1>All users</h1>
<ul class="users">
<% @users.each do |user| %>
<li>
<%= gravatar_for user, size: 50 %>
<%= link_to user.name, user %>
</li>
<% end %>
</ul>
c). 画布局
//app/assets/stylesheets/custom.css.scss
...
/* Users index */
.users {
list-style: none;
margin: 0;
li {
overflow: auto;
padding: 10px 0;
border-bottom: 1px solid $gray-lighter;
}
}
d). header中增加link
//app/views/layouts/_header.html.erb
<% if logged_in? %>
<li><%= link_to "Users", users_path %></li>
<li class="dropdown">
...
polen:
运行中如果有人遇到undefined method
each' for nil:NilClass`,错误代码停留在:
<% @users.each do |user| %>
这个是因为你controller中的index方法下面没有@users = User.all
这句话. (这个错误是提醒你没有'each'这个方法,这个'each'是@users在调用,所以问题出在@user上面)
正确的代码是:
def index
@users = User.all
end
按照上面的ABC进行写代码是没有问题的,我之前因为上来就是画UI,controller忘记写了,所以出问题了,也算是涨姿势了.
9.3.2 Sample users
制造一些"假人"
a). gem file 引入,并bundle install
gem 'faker', '1.4.2'
b). 添加一个rake task
//db/seeds.rb
User.create!(name: "Example User",
email: "example@railstutorial.org",
password: "foobar",
password_confirmation: "foobar")
19.times do |n|
name = Faker::Name.name
email = "example-#{n+1}@railstutorial.org"
password = "password"
User.create!(name: name,
email: email,
password: password,
password_confirmation: password)
end
c). rake 执行数据库
$ bundle exec rake db:migrate:reset
$ bundle exec rake db:seed
9.3.3 Pagination
用户比较多,大家都挤在第一页怎么办,加一个分页呗.
a). 库先引用起来啊:
gem 'will_paginate', '3.0.7'
gem 'bootstrap-will_paginate', '0.0.10'
b). index的UI画起来了啊:
//app/views/users/index.html.erb
<% provide(:title, 'All users') %>
<h1>All users</h1>
<%= will_paginate %>
<ul class="users">
<% @users.each do |user| %>
<li>
<%= gravatar_for user, size: 50 %>
<%= link_to user.name, user %>
</li>
<% end %>
</ul>
<%= will_paginate %>
c). controller中的user就需要按照分页来取值了啊
//app/controllers/users_controller.rb
class UsersController < ApplicationController
...
def index
@users = User.paginate(page: params[:page])
end
9.3.5 Partial refactoring
重构代码是进步的开端...
polen:比较懒,直接贴图了:
Partial refactoring
9.4 Deleting users
9.4.1 Administrative users
管理员用户
rails generate migration add_admin_to_users admin:boolean
//db/migrate/[timestamp]_add_admin_to_users.rb
class AddAdminToUsers < ActiveRecord::Migration
def change
add_column :users, :admin, :boolean, default: false
end
end
9.4.2 The destroy action
删除用户的操作:
a) .画UI
//app/views/users/_user.html.erb
<li>
<%= gravatar_for user, size: 50 %>
<%= link_to user.name, user %>
<% if current_user.admin? && !current_user?(user) %>
| <%= link_to "delete", user, method: :delete,
data: { confirm: "You sure?" } %>
<% end %>
</li>
b). controller增加destroy:
class UsersController < ApplicationController
before_action :logged_in_user, only: [:edit, :update]
before_action :correct_user, only: [:edit, :update]
before_action :admin_user, only: :destroy
...
def destroy
User.find(params[:id]).destroy
flash[:success] = "User deleted"
redirect_to users_url
end
private
...
# Confirms an admin user.
def admin_user
redirect_to(root_url) unless current_user.admin?
end
...
<br />
10. Account activation and password reset
这章是关于用户激活(验证email)和密码重设的功能("忘记密码")
10.1 Account activation
10.1.1 Account activations resource
a). 首先我们需要新建个controller:AccountActivations
rails generate controller AccountActivations --no-test-framework
b). 路由是要配置的喽:
屏幕快照 2016-05-30 下午9.22.36.png
c). 生成一个add_activation_to_users 数据表
rails generate migration add_activation_to_users activation_digest:string activated:boolean activated_at:datetime
打开生成的add_activation_to_users.rb,加入add_column :users, :activated, :boolean, default: false
d). model中增加一个create_activation_digest:
create_activation_digest
10.1.2 Account activation mailer method
激活邮箱依赖一个Action Mailer library,用户需要使用激活码+email来激活账户
a). 需要一个mailer
rails generate mailer UserMailer account_activation password_reset
(重设密码方法这里也一并添加进去)
b). 修改user_mailer.rb中的account_activation方法:
(application_mailer.rb修改发件人邮箱的,自行修改下就好了,这里略过)
//app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer
# Subject can be set in your I18n file at config/locales/en.yml
# with the following lookup:
#
# en.user_mailer.account_activation.subject
#
def account_activation(user)
@user = user
mail to: user.email, subject: "Account activation"
end
...
c). 修改下UI:
The account activation view
d). 修改环境配置:
//config/environments/development.rb
Rails.application.configure do
...
# Don't care if the mailer can't send.
config.action_mailer.raise_delivery_errors = true
config.action_mailer.delivery_method = :test
host = 'localhost:3000'
config.action_mailer.default_url_options = { host: host, protocol: 'http' }
...
10.1.3 Activating the account
//app/models/user.rb
...
# Returns true if the given token matches the digest.
def authenticated?(attribute, token)
digest = send("#{attribute}_digest")
return false if digest.nil?
BCrypt::Password.new(digest).is_password?(token)
end
...
polen:
send 是ruby的一个动态方法,可用于发送消息.
官方解释是:
Invokes the method identified by aSymbol, passing it any arguments specified. You can use send if the name send clashes with an existing method in obj.
但这里需要注意的是,send方法太过强大,可以调用任何方法,包括私有方法,使用public_send方法将能够尊重方法接受者的隐私权,可以用它来代替send方法
参考:浅析 Ruby 里的几个动态方法 (一),send 方法
//app/helpers/sessions_helper.rb
...
# Returns the user corresponding to the remember token cookie.
def current_user
if (user_id = session[:user_id])
@current_user ||= User.find_by(id: user_id)
elsif (user_id = cookies.signed[:user_id])
user = User.find_by(id: user_id)
if user && user.authenticated?(:remember, cookies[:remember_token])
log_in user
@current_user = user
end
end
end
...
//app/controllers/account_activations_controller.rb
class AccountActivationsController < ApplicationController
def edit
user = User.find_by(email: params[:email])
if user && !user.activated? && user.authenticated?(:activation, params[:id])
user.update_attribute(:activated, true)
user.update_attribute(:activated_at, Time.zone.now)
log_in user
flash[:success] = "Account activated!"
redirect_to user
else
flash[:danger] = "Invalid activation link"
redirect_to root_url
end
end
end
以上完成了激活功能,这样我们就可增加类似 "如果用户没有激活那么就没法登陆"这类的限制了
//app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
...
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
if user.activated?
log_in user
params[:session][:remember_me] == '1' ? remember(user) : forget(user)
redirect_back_or user
else
message = "Account not activated. "
message += "Check your email for the activation link."
flash[:warning] = message
redirect_to root_url
end
else
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end
...
10.2 Password reset
10.2.1 Password resets resource
a). 新建一个PasswordResets controller:
rails generate controller PasswordResets new edit --no-test-framework
b). 老规矩,修改路由:
//config/routes.rb
Rails.application.routes.draw do
root 'static_pages#home'
get 'help' => 'static_pages#help'
get 'about' => 'static_pages#about'
get 'contact' => 'static_pages#contact'
get 'signup' => 'users#new'
get 'login' => 'sessions#new'
post 'login' => 'sessions#create'
delete 'logout' => 'sessions#destroy'
resources :users
resources :account_activations, only: [:edit]
resources :password_resets, only: [:new, :create, :edit, :update]
...
c). 改UI,增加link_to "(forgot password)"
//app/views/sessions/new.html.erb
...
<%= f.label :password %>
<%= link_to "(forgot password)", new_password_reset_path %>
<%= f.password_field :password, class: 'form-control' %>
...
10.2.2 Password resets controller and form
a). login表单UI再修改下:
//app/views/sessions/new.html.erb
<% provide(:title, "Log in") %>
<h1>Log in</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(:session, url: login_path) do |f| %>
<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.label :remember_me, class: "checkbox inline" do %>
<%= f.check_box :remember_me %>
<span>Remember me on this computer</span>
<% end %>
<%= f.submit "Log in", class: "btn btn-primary" %>
<% end %>
<p>New user? <%= link_to "Sign up now!", signup_path %></p>
</div>
</div>
b). 画UI, 重设密码
//app/views/password_resets/new.html.erb
<% provide(:title, "Forgot password") %>
<h1>Forgot password</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(:password_reset, url: password_resets_path) do |f| %>
<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>
<%= f.submit "Submit", class: "btn btn-primary" %>
<% end %>
</div>
</div>
d). password reset 增加个create action
//app/controllers/password_resets_controller.rb
...
def create
@user = User.find_by(email: params[:password_reset][:email].downcase)
if @user
@user.create_reset_digest
@user.send_password_reset_email
flash[:info] = "Email sent with password reset instructions"
redirect_to root_url
else
flash.now[:danger] = "Email address not found"
render 'new'
end
end
...
e). user model 增加send_password_reset_email和create_reset_digest
//app/models/user.rb
...
# Sets the password reset attributes.
def create_reset_digest
self.reset_token = User.new_token
update_attribute(:reset_digest, User.digest(reset_token))
update_attribute(:reset_sent_at, Time.zone.now)
end
# Sends password reset email.
def send_password_reset_email
UserMailer.password_reset(self).deliver_now
end
private
...
10.2.3 Password reset mailer method
重设密码发出邮件的文本,对应操作等.
Password reset mailer method
10.2.4 Resetting the password
a). 走个UI:
//app/views/password_resets/edit.html.erb
<% provide(:title, 'Reset password') %>
<h1>Reset password</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(@user, url: password_reset_path(params[:id])) do |f| %>
<%= render 'shared/error_messages' %>
<%= hidden_field_tag :email, @user.email %>
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.label :password_confirmation, "Confirmation" %>
<%= f.password_field :password_confirmation, class: 'form-control' %>
<%= f.submit "Update password", class: "btn btn-primary" %>
<% end %>
</div>
</div>
b). controller 加入 action
//app/controllers/password_resets_controller.rb
class PasswordResetsController < ApplicationController
before_action :get_user, only: [:edit, :update]
before_action :valid_user, only: [:edit, :update]
before_action :check_expiration, only: [:edit, :update]
def new
end
def create
@user = User.find_by(email: params[:password_reset][:email].downcase)
if @user
@user.create_reset_digest
@user.send_password_reset_email
flash[:info] = "Email sent with password reset instructions"
redirect_to root_url
else
flash.now[:danger] = "Email address not found"
render 'new'
end
end
def edit
end
def update
if params[:user][:password].empty?
@user.errors.add(:password, "can't be empty")
render 'edit'
elsif @user.update_attributes(user_params)
log_in @user
flash[:success] = "Password has been reset."
redirect_to @user
else
render 'edit'
end
end
private
def user_params
params.require(:user).permit(:password, :password_confirmation)
end
# Before filters
def get_user
@user = User.find_by(email: params[:email])
end
# Confirms a valid user.
def valid_user
unless (@user && @user.activated? &&
@user.authenticated?(:reset, params[:id]))
redirect_to root_url
end
end
# Checks expiration of reset token.
def check_expiration
if @user.password_reset_expired?
flash[:danger] = "Password reset has expired."
redirect_to new_password_reset_url
end
end
end
c). 检查是否过期
//app/models/user.rb
...
# Returns true if a password reset has expired.
def password_reset_expired?
reset_sent_at < 2.hours.ago
end
...
okey 以上完成了用户登陆,注册,用户更新信息,忘记密码,账户激活等功能.算是一个大篇章完结.
<br />
Github:
本文所有的代码已上传github:
polegithub/rails_sample_app_polen
相关:
[Ruby]《Ruby on Rails Tutorial》的搬运工之一
[Ruby]《Ruby on Rails Tutorial》的搬运工之三
by poles