about 2 years ago

重點:Action Mailer library
重點:boolean要從false變成true,可以用update_attribute => Listing 10.29: app/controllers/account_activations_controller.rb
重點:參考self的解釋:When to use self in Model?
self
Ruby當中的class method和instance method差在哪?
[rails筆記] ruby 觀念小整理

  1. Account activation:和passwords (Section 8.2) 跟 remember tokens (Section 8.4)的作法很相似。

  2. account activations,不需要用到model (Active Record) (sessions也不用),所以可以直接加在User Model裡面。resources :account_activations, only: [:edit]
    As with sessions (Section 8.1), we’ll model account activations as a resource even though they won’t be associated with an Active Record model. Instead, we’ll include the relevant data (including the activation token and activation status) in the User model. Nevertheless, we’ll interact with account activations via a standard REST URL; because the activation link will be modifying the user’s activation status, we’ll plan to use the edit action.

  3. 預設是false
    add_column :users, :activated, :boolean, default: false

  4. 每個user都需要activation,所以要在user被created之前,就需要先產生activation token and activation digest
    Because every newly signed-up user will require activation, we should assign an activation token and digest to each user object before it’s created.

  5. before_save:每次存檔前(包含create跟update)都會啟動;
    before_create:只有在create之前才會啟動。
    A before_save callback is automatically called before the object is saved, which includes both object creation and updates, but in the case of the activation digest we only want the callback to fire when the user is created. This requires a before_create callback, which we’ll define as follows:

  6. before_create :create_activation_digestmethod reference:在create新用戶前,會先去找這個method。
    This code, called a method reference, arranges for Rails to look for a method called create_activation_digest and run it before creating the user.

  7. create_activation_digest只會被User Model用到,所以要放在private下面。
    Because the create_activation_digest method itself is only used internally by the User model, there’s no need to expose it to outside users; as we saw in Section 7.3.2, the Ruby way to accomplish this is to use the private keyword:

  8. 這邊是借用到8.32的概念 (筆記)因為,activation_token是一個不存在的attribute,所以可以需要用self去新增他,然後new_token這個方法不會用到object,所以他是一個Class method,裡面已經寫好了User.new_token SecureRandom.urlsafe_base64
    This code simply reuses the token and digest methods used for the remember token, as we can see by comparing with the remember method from Listing 8.32:

    self.activation_token  = User.new_token
    self.activation_digest = User.digest(activation_token)
    
    def User.new_token
    SecureRandom.urlsafe_base64
    end
    
  9. 有了user之後,才會有remember token這個功能。
    可是,使用activation_token的時候,還沒有正式新增user。
    所以兩個寫法不同。
    當user註冊的時候,會因為before_create這個功能先去產生activation_token and activation_digest,然後activation_digest會被新增到資料庫裡面去。

    self.activation_token  = User.new_token
    self.activation_digest = User.digest(activation_token)
    
    def remember
    self.remember_token = User.new_token
    update_attribute(:remember_digest, User.digest(remember_token))
    end
    

    The main difference is the use of update_attribute in the latter case. The reason for the difference is that remember tokens and digests are created for users that already exist in the database, whereas the before_create callback happens before the user has been created. As a result of the callback, when a new user is defined with User.new (as in user signup, Listing 7.17), it will automatically get both activation_token and activation_digest attributes; because the latter is associated with a column in the database (Figure 10.1), it will be written automatically when the user is saved.

  10. Action Mailer library:$ rails generate mailer UserMailer account_activation password_reset
    (account_activation password_reset 這兩個是method,分別都會有兩個view,一個是純文字檔,一個是HTML檔)
    Mailers跟controller actions很像,會有view。
    With the data modeling complete, we’re now ready to add the code needed to send an account activation email. The method is to add a User mailer using the Action Mailer library, which we’ll use in the Users controller create action to send an email with an activation link. Mailers are structured much like controller actions, with email templates defined as views. Our task in this section is to define the mailers and views with links containing the activation token and email address associated with the account to be activated.
    Here we’ve generated the necessary account_activation method as well as the password_reset method we’ll need in Section 10.2.
    As part of generating the mailer, Rails also generates two view templates for each mailer, one for plain-text email and one for HTML email. For the account activation mailer method, they appear as in Listing 10.6 and Listing 10.7.

  11. 寄出去的啟用信,要包含user的名字(用email找到user的id),以及ativation link。
    As with ordinary views, we can use embedded Ruby to customize the template views, in this case greeting the user by name and including a link to a custom activation link. Our plan is to find the user by email address and then authenticate the activation token, so the link needs to include both the email and the token. Because we’re modeling activations using an Account Activations resource, the token itself can appear as the argument of the named route defined in Listing 10.1:edit_account_activation_url(@user.activation_token, ...)

  12. Listing 10.21:新增用戶時,如果user_param獲得通過,@user若是被save,UserMailer則會執行account_activation這個method,會把@user送到account_activation去。

    app/controllers/users_controller.rb
    class UsersController < ApplicationController
    def create
    @user = User.new(user_params)
    if @user.save
    UserMailer.account_activation(@user).deliver_now
    flash[:info] = "Please check your email to activate your account."
    redirect_to root_url
    else
    render 'new'
    end
    end
    end
    
  13. Listing 10.11: (這邊的@user是要送到view去用的)

    app/mailers/user_mailer.rb
    class UserMailer < ApplicationMailer
    def account_activation(user)
    @user = user
    mail to: user.email, subject: "Account activation"
    end
    end
    
  14. Listing 10.12: user_mailer.rb送過來的@user,會被用在新用戶的啟動信裡面。

    app/views/user_mailer/account_activation.text.erb
    Hi <%= @user.name %>,
    Welcome to the Sample App! Click on the link below to activate your account:
    <%= edit_account_activation_url(@user.activation_token, email: @user.email) %>
    

    這邊,應該是會被送到 Listing 10.29: app/controllers/account_activations_controller.rbedit method

  15. 開始寫edit前的準備工作:
    10.1.3 Activating the account:作法會跟passwords (Listing 8.5)remember tokens (Listing 8.36)類似

    app/controllers/sessions_controller.rb
    user = User.find_by(email: params[:email])
    if user && user.authenticated?(:activation, params[:id])
    

    不過,這邊是寫死的,所以要改成通用的寫法。

    app/models/user.rb(Listing 8.33)
    def authenticated?(remember_token)
    return false if remember_digest.nil?
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
    end
    

    可以用send去改成這樣:

    app/models/user.rb (Listing 10.24)
    def authenticated?(attribute, token)
    digest = self.send("#{attribute}_digest")
    return false if digest.nil?
    BCrypt::Password.new(digest).is_password?(token)
    end
    

    因為是在User Model裡面,所以可以把self省略,又可以變成這樣:
    Because we’re inside the user model, we can also omit self, yielding the most idiomatically correct version:

    app/models/user.rb (Listing 10.24)
    def authenticated?(attribute, token)
    digest = send("#{attribute}_digest")
    return false if digest.nil?
    BCrypt::Password.new(digest).is_password?(token)
    end
    

    (如果沒有token這個變數,可以放''進去,如Listing 10.27)

  16. metaprogramming:用來寫程式的程式
    metaprogramming, which is essentially a program that writes a program.

  17. send method:送出訊息。
    send method, which lets us call a method with a name of our choice by “sending a message” to a given object.

    >> user = User.first
    >> user.activation_digest
    => "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtRcp6UZKRM6N9Ae"
    >> user.send(:activation_digest)
    => "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtRcp6UZKRM6N9Ae"
    >> user.send('activation_digest')
    => "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtRcp6UZKRM6N9Ae"
    >> attribute = :activation
    >> user.send("#{attribute}_digest")
    => "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtRcp6UZKRM6N9Ae"
    
  18. authenticated?寫好後,就可以開始寫edit了:
    With the authenticated? method as in Listing 10.24, we’re now ready to write an edit action that authenticates the user corresponding to the email address in the params hash. Our test for validity will look like this:if user && !user.activated? && user.authenticated?(:activation, params[:id])
    !user.activated?的寫法是要避免已經啟用的account再被啟用ㄧ次。
    Note the presence of !user.activated?, which is the extra boolean alluded to above. This prevents our code from activating users who have already been activated, which is important because we’ll be logging in users upon confirmation, and we don’t want to allow attackers who manage to obtain the activation link to log in as the user.
    If the user is authenticated according to the booleans above, we need to activate the user and update the activated_at timestamp

  19. 新註冊者收到啟用信之後,點選連結,會把activation_tokenemail送回來edit method,如果通過檢驗(authenticated?),就會把:activatedupdatetrue,順便把啟用時間也補上。

    app/views/user_mailer/account_activation.html.erb (Listing 10.13)
    <h1>Sample App</h1>
    <p>Hi <%= @user.name %>,</p>
    <p>Welcome to the Sample App! Click on the link below to activate your account:</p>
    <%= link_to "Activate", edit_account_activation_url(@user.activation_token,
                                                    email: @user.email) %>
    
    app/controllers/account_activations_controller.rb (Listing 10.29)
    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/models/user.rb (Listing 10.24)
    def authenticated?(attribute, token)
    digest = send("#{attribute}_digest")
    return false if digest.nil?
    BCrypt::Password.new(digest).is_password?(token)
    end
    
  20. 要讓activation實際有用,就要把他放在登入的地方:有啟用過的用戶才能登入。

    app/controllers/sessions_controller.rb (Listing 10.30)
    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
    end
    
  21. refactor:重構
    Eddie: 可以的話,controller 儘量精簡一些,把實作的邏輯藏到別的地方去(例如 model、service object、form object..etc)
    With the test in Listing 10.31, we’re ready to refactor a little by moving some of the user manipulation out of the controller and into the model. In particular, we’ll make an activate method to update the user’s activation attributes and a send_activation_email to send the activation email. The extra methods appear in Listing 10.33, and the refactored application code appears in Listing 10.34 and Listing 10.35.
    這邊的UserMailer.account_activation(@user).deliver_now可以拿到user model,在model新增一個send_activation_email的method

    app/controllers/users_controller.rb
    class UsersController < ApplicationController
    def create
    @user = User.new(user_params)
    if @user.save
    UserMailer.account_activation(@user).deliver_now
    flash[:info] = "Please check your email to activate your account."
    redirect_to root_url
    else
    render 'new'
    end
    end
    end
    
    app/controllers/users_controller.rb (Listing 10.34)
     class UsersController < ApplicationController
    .
    .
    .
    def create
    @user = User.new(user_params)
    if @user.save
      @user.send_activation_email
      flash[:info] = "Please check your email to activate your account."
      redirect_to root_url
    else
      render 'new'
    end
    end
    .
    .
    .
    end
    

    這邊的user.update_attribute可以拿到user model,在model新增一個activate的method

    app/controllers/account_activations_controller.rb (Listing 10.29)
    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/account_activations_controller.rb (Listing 10.35)
    class AccountActivationsController < ApplicationController
    def edit
    user = User.find_by(email: params[:email])
    if user && !user.activated? && user.authenticated?(:activation, params[:id])
      user.activate
      log_in user
      flash[:success] = "Account activated!"
      redirect_to user
    else
      flash[:danger] = "Invalid activation link"
      redirect_to root_url
    end
    end
    end
    
    app/models/user.rb (Listing 10.33)
    class User < ActiveRecord::Base
    def activate
    update_attribute(:activated,    true)
    update_attribute(:activated_at, Time.zone.now)
    end
    def send_activation_email
    UserMailer.account_activation(self).deliver_now
    end
    end
    

    activate methoduser.update_attribute要把user拿掉,可以改成self,不過self在model內又可以被省略。
    We could have switched from user to self, but recall from Section 6.2.5 that self is optional inside the model.

  22. 我對self的理解:
    等號右邊的self是可以省落的。(self.email = email.downcase)
    當在一個model裡面時,如果不想新增一個新的變數的話,左邊就一定要寫self (self.remember_token)
    Section 10.1.4(在model裡面可以省略self,update_attribute(:activated, true))跟 Section 6.2.5有點矛盾(左邊的self不可省略,email = email.downcase wouldn’t work.),
    Section 6.2.5:self is optional inside the model.
    Section 4.4.2:reverse in the palindrome method
    Section 8.4:self is not optional in an assignment:
    如果要指定自己的attribute,或是不想新增一個local variable的話,就要記得加self。
    不過,只是update_attribute的話,(好像)就不用加self

    class User < ActiveRecord::Base
    attr_accessor :remember_token
    .
    .
    .
    def remember
    self.remember_token = ...
    update_attribute(:remember_digest, ...)
    end
    end
    
    -user.update_attribute(:activated,    true)
    -user.update_attribute(:activated_at, Time.zone.now)
    +update_attribute(:activated,    true)
    +update_attribute(:activated_at, Time.zone.now)
    

    (We could have switched from user to self, but recall from Section 6.2.5 that self is optional inside the model.)

  23. Password reset:
    登入的地方(sessions/new.html.erb)加上"忘記密碼的連結",
    點選後會到一個新的view(views/password_resets/new.html.erb)去輸入email,
    系統(controllers/password_resets_controller.rbcreate method)會根據email找出來這個user,然後會產生digest(存在資料庫)跟token(寄給用戶),之後會寄出一封信,包含一個內含email跟token的連結。當
    用戶點連結之後,會將用戶的email跟token傳送回去edit method作檢查(被分拆掉了,可以看第26點),如果這個用戶是有效的(valid_user)話,就顯示變更密碼的頁面(Figure 10.17),
    戶輸入新密碼(兩次)後,會把新密碼傳回去password_resets_controller.rbupdate method去更新密碼。

  24. 點選忘記密碼的連結,會跑到views/password_resets/new.html.erb的頁面。
    view裡面是這樣寫的:

    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>
    

    這樣的寫法,會把user的email傳回去資料庫,
    (也可以用params[:password_reset][:email]叫出這個user的email)
    所以new之後,會跑到create去,先用輸入的email找看看有沒有這個user?
    如果有的話,就先為他建一個新的reset_digest,然後寄信給這個user。
    (這邊又要先跑到user那邊先寫create_reset_digestsend_password_reset_email

    app/controllers/password_resets_controller.rb
    class PasswordResetsController < ApplicationController
    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
    end
    
    app/models/user.rb
     class User < ActiveRecord::Base
    attr_accessor :remember_token, :activation_token, :reset_token
    before_save   :downcase_email
    before_create :create_activation_digest
    # Activates an account.
    
    def activate
    update_attribute(:activated,    true)
    update_attribute(:activated_at, Time.zone.now)
    end
    # Sends activation email.
    
    def send_activation_email
    UserMailer.account_activation(self).deliver_now
    end
    # 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
    # Converts email to all lower-case.
    
    def downcase_email
      self.email = email.downcase
    end
    # Creates and assigns the activation token and digest.
    
    def create_activation_digest
      self.activation_token  = User.new_token
      self.activation_digest = User.digest(activation_token)
    end
    end
    
  25. 可以參考10.1.2 Account activation mailer method
    先按忘記密碼(new),輸入email會到create去產生新的token跟寄信,重設密碼的信裡面會有一個到edit method的link:
    edit_password_reset_url(@user.reset_token, email: @user.email)
    http://example.com/password_resets/3BdBrXeQZSWqFIDRN8cxHA/edit?email=foo%40bar.com
    可以想作:
    edit_user_url
    ,會變成:
    http://example.com/user/1/edit
    這一串亂碼是由new_token產生的,可以當作ID來用。the token will be available in the params hash as params[:id].
    (這邊的reset_token好像沒有加密,忘記由new_token產生的亂碼有沒有加密過了,好像reset_digest = 'digest'(reset_token)這裏的'digest'才會把東西做加密)
    感覺好像是去修改編號(1)user的個人資料。
    感覺好像是去修改編號(亂碼)password_reset的個人資料。
    (這邊記得要有hidden field,才能把email傳回去controller)

  26. 點下去edit_password_reset_url(@user.reset_token, email: @user.email)之後,會回到填寫兩次密碼的地方。兩次密碼輸入之後,會回到update method去做驗證。

  27. activation的時候,只有edit method,可是沒有update method,好像是因為他在edit裡面就直接用user.update_attribute(:activated, true)了(Listing 10.29: App/controllers/account_activations_controller.rb)

  28. password_reset的時候,只有update method,沒有edit method,好像是因為他把edit裡面需要用到的找到user(get_user method)跟認證user (valid_user)都先用before_action處理過了。

    app/controllers/password_resets_controller.rb (Listing 10.51)
    class PasswordResetsController < ApplicationController
    before_action :get_user,   only: [:edit, :update]
    before_action :valid_user, only: [:edit, :update]
    def edit
    end
    private
    def get_user
      @user = User.find_by(email: params[:email])
    end
    def valid_user
      unless (@user && @user.activated? &&
              @user.authenticated?(:reset, params[:id]))
        redirect_to root_url
      end
    end
    end
    
  29. 點下去edit_password_reset_url(@user.reset_token, email: @user.email)之後,會回到填寫兩次密碼的地方。兩次密碼輸入之後,會回到update method去做驗證。
    然後這邊要考慮四種如果:時間過期,成功更新密碼,更新密碼失敗(無效的密碼),更新密碼失敗(確認欄是空白的)
    To define the update action corresponding to the edit action in Listing 10.51, we need to consider four cases: an expired password reset, a successful update, a failed update (due to an invalid password), and a failed update (which initially looks “successful”) due to a blank password and confirmation.

    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
    
← ROR TUTORIAL (3RD ED.) Ch9:Updating, showing, and deleting users ROR TUTORIAL (3RD ED.) Ch11 User microposts →