[Ruby]《Ruby on Rails Tutorial》的搬运工之二

[Ruby]《Ruby on Rails Tutorial》的搬运工之二_第1张图片
ruby-on-rails-tutorials.jpg

背景:

  1. 最近比较闲,想学习ruby on rails
  2. 于是找到了https://www.railstutorial.org 上的首推教程《Ruby on Rails Tutorial》
    [Ruby]《Ruby on Rails Tutorial》的搬运工之二_第2张图片
    屏幕快照 2016-05-29 上午11.04.20.png

    这本书第一章和第二章讲了2个基本demo,实在没啥意思,姑且略过. 从第三章开始到第十二章是从0到1实现了一个类似Twitter的简单社交网站(首页,登录注册,发布推文,关注等功能). 怎么样是不是很棒?
    但是这个本书实在讲得过于详细,对于我这种本身没有那么多时间(也没那么多耐心)去一点一点看下来的童鞋,看着实在太着急了,于是准备快速整理下(把里面的干货和代码提取出来),方便大家可以分分钟coding出这个demo出来.
    当然真正学习还是要看原教程,我这个只是"扒皮版本".


原文链接

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


ruby学习框架图

[Ruby]《Ruby on Rails Tutorial》的搬运工之二_第3张图片
ruby on rails is hard?

第3-7章节见:

[Ruby]RUBY ON RAILS TUTORIAL 的搬运工第一天


下面是第8章开始



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") %>

Log in

<%= 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 %>

New user? <%= link_to "Sign up now!", signup_path %>

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


这里的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


[Ruby]《Ruby on Rails Tutorial》的搬运工之二_第4张图片
屏幕快照 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,如下:


[Ruby]《Ruby on Rails Tutorial》的搬运工之二_第5张图片
“Remember me” checkbox

a). 直接修改new.html

vim app/views/sessions/new.html.erb
<% provide(:title, "Log in") %>

Log in

<%= 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 %> Remember me on this computer <% end %> <%= f.submit "Log in", class: "btn btn-primary" %> <% end %>

New user? <%= link_to "Sign up now!", signup_path %>

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;


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") %>

Update your profile

<%= 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 %>
<%= gravatar_for @user %> change

c). 首页导航栏的setting可以完善url了

vim app/views/layouts/_header.html.erb

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的通讯录,好友列表,粉丝列表都会遇到...)


[Ruby]《Ruby on Rails Tutorial》的搬运工之二_第6张图片
屏幕快照 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') %>

All users

    <% @users.each do |user| %>
  • <%= gravatar_for user, size: 50 %> <%= link_to user.name, user %>
  • <% end %>

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? %>
       
  • <%= link_to "Users", users_path %>
  • polen:
    运行中如果有人遇到undefined methodeach' 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: "[email protected]",
                 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') %>
    

    All users

    <%= will_paginate %>
      <% @users.each do |user| %>
    • <%= gravatar_for user, size: 50 %> <%= link_to user.name, user %>
    • <% end %>
    <%= 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:比较懒,直接贴图了:


    [Ruby]《Ruby on Rails Tutorial》的搬运工之二_第7张图片
    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
    
  • <%= 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 %>
  • 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
    
    ...
    


    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). 路由是要配置的喽:


    [Ruby]《Ruby on Rails Tutorial》的搬运工之二_第8张图片
    屏幕快照 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

    [Ruby]《Ruby on Rails Tutorial》的搬运工之二_第9张图片
    屏幕快照 2016-05-30 下午9.29.23.png

    d). model中增加一个create_activation_digest:


    [Ruby]《Ruby on Rails Tutorial》的搬运工之二_第10张图片
    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:


    [Ruby]《Ruby on Rails Tutorial》的搬运工之二_第11张图片
    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") %>
    

    Log in

    <%= 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 %> Remember me on this computer <% end %> <%= f.submit "Log in", class: "btn btn-primary" %> <% end %>

    New user? <%= link_to "Sign up now!", signup_path %>

    b). 画UI, 重设密码

    //app/views/password_resets/new.html.erb
    <% provide(:title, "Forgot password") %>
    

    Forgot password

    <%= 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 %>

    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

    重设密码发出邮件的文本,对应操作等.


    [Ruby]《Ruby on Rails Tutorial》的搬运工之二_第12张图片
    Password reset mailer method

    10.2.4 Resetting the password

    a). 走个UI:

    //app/views/password_resets/edit.html.erb
    <% provide(:title, 'Reset password') %>
    

    Reset password

    <%= 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 %>

    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 以上完成了用户登陆,注册,用户更新信息,忘记密码,账户激活等功能.算是一个大篇章完结.


    Github:


    本文所有的代码已上传github:
    polegithub/rails_sample_app_polen

    相关:


    [Ruby]《Ruby on Rails Tutorial》的搬运工之一
    [Ruby]《Ruby on Rails Tutorial》的搬运工之三


    by poles

    你可能感兴趣的:([Ruby]《Ruby on Rails Tutorial》的搬运工之二)