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

ruby-on-rails-tutorials.jpg

背景:

  1. 最近比较闲,想学习ruby on rails
  2. 于是找到了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 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: "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

屏幕快照 2016-05-30 下午9.29.23.png

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容