Monday, July 17, 2017

Completed Ch 11: Account activation

* I created a new branch called: account-activation
$ git checkout -b account-activation

* I generated an Account Activation controller.
$ rails generate controller AccountActivations

* I added a route for account activation.
config/routes.rb
  resources :account_activations, only: [:edit]

* In the Users data model, I added activation_digest, activated, and activated_at fields.
$ rails generate migration add_activation_to_users \
> activation_digest:string activated:boolean activated_at:datetime

* In the DB migration file, I added the default value = false for the activated field.
sample_app/db/migrate
class AddActivationToUsers < ActiveRecord::Migration[5.0]
  def change
    add_column :users, :activation_digest, :string
    add_column :users, :activated, :boolean, default: false
    add_column :users, :activated_at, :datetime
  end
end

* Then, I ran DB migrate command.
$ rails db:migrate

* In the User model, I added account activation code.
sample_app/app/models/user.rb
class User < ApplicationRecord
  attr_accessor :remember_token, :activation_token
  before_save   :downcase_email
  before_create :create_activation_digest
  validates :name,  presence: true, length: { maximum: 50 }
  .
  .
  .
  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

* In DB Seeds file, I set the status the admin user and first 99 users to activated = true.
sample_app/db/seeds.rb
activated: true,
activated_at: Time.zone.now)

* I set the activated = true status to the test fixture file too.
sample_app/test/fixtures/users.yml
  activated: true
  activated_at: <%= Time.zone.now %>

* Then, I reset the DB and re-generated the seeds file.
$ rails db:migrate:reset
$ rails db:seed

* The next section was generating the mailer for account_activation and password_reset.
$ rails generate mailer UserMailer account_activation password_reset

* I updated the from address of Application Mailer.
sample_app/app/mailers/application_mailer.rb
  default from: "noreply@example.com"

* I updated the mailing template for User Mailer.
sample_app/app/mailers/user_mailer.rb
  def account_activation(user)
    @user = user
    mail to: user.email, subject: "Account activation"
  end

* I drafted the email template in Text file for accound activation.
sample_app/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) %>

* I did it for HTML version as well.
sample_app/app/views/user_mailer/account_activation.html.erb
<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) %>


* I updated the config environment.
sample_app/config/environments/development.rb
  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' }
   
  config.action_mailer.perform_caching = false

* Then, I re-started my server.
Keyboard: Ctrl + C
$ rail s -b 0.0.0.0 -p 3000

* I updated Mailer preview file.
sample_app/test/mailers/previews/user_mailer_preview.rb

* I previewed the Mailer template. In Firefox, I typed: http://localhost:3000/rails/mailers/user_mailer/account_activation.html



* I copied the Account Activation test case for User Mailer.
sample_app/test/mailers/user_mailer_test.rb
  test "account_activation" do
    user = users(:michael)
    user.activation_token = User.new_token
    mail = UserMailer.account_activation(user)
    assert_equal "Account activation", mail.subject
    assert_equal [user.email], mail.to
    assert_equal ["noreply@example.com"], mail.from
    assert_match user.name,               mail.body.encoded
    assert_match user.activation_token,   mail.body.encoded
    assert_match CGI.escape(user.email),  mail.body.encoded
  end

* In Test Environment, I added the defaul_url_options to example.com
sample_app/config/environments/test.rb
  config.action_mailer.default_url_options = { host: 'example.com' }

* I editted the def create action of Users Controller.
sample_app/app/controllers/users_controller.rb
  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

* I editted the User Signup Integration Test Suite.
test/integration/users_signup_test.rb
  test "invalid signup information" do
   assert_select 'div#error_explanation'
    assert_select 'div.field_with_errors'
  end

  test "valid signup information" do
    # assert_template 'users/show'
    # assert is_logged_in?
  end

* I added the def authenticated?(attribute, token) method in User model.
sample_app/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

* I updated a line in Sessions Helper.
sample_app/app/helpers/sessions_helper.rb
      if user && user.authenticated?(:remember, cookies[:remember_token])

* I updated the test case "authenticated? should return false for a user with nil digest" in User Model Test Suite.
sample_app/test/models/user_test.rb
    assert_not @user.authenticated?(:remember, '')

* I added the edit action for Account Activation Controller.
sample_app/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

* I pasted the following URL from the server log file to activate the new user. In Firefox, I pasted this URL:
http://localhost:3000/account_activations/UB_1LRUTcNuhOPiMvF6HvQ/edit?email=jimmyc%40example.com

* The activation was successful.


* I updated the create action in Sessions Controller.
sample_app/app/controllers/sessions_controller.rb
  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


* I edited the User SignUp Integration Test.
sample_app/test/integration/users_signup_test.rb
require 'test_helper'

class UsersSignupTest < ActionDispatch::IntegrationTest

  def setup
    ActionMailer::Base.deliveries.clear
  end
       
  test "invalid signup information" do
    get signup_path
    assert_no_difference 'User.count' do
      post users_path, params: { user: { name:  "",
                                         email: "user@invalid",
                                         password:              "foo",
                                         password_confirmation: "bar" } }
    end
    assert_template 'users/new'
    assert_select 'div#error_explanation'
    assert_select 'div.field_with_errors'     
  end   

  test "valid signup information with account activation" do
    get signup_path
    assert_difference 'User.count', 1 do
      post users_path, params: { user: { name:  "Example User",
                                         email: "user@example.com",
                                         password:              "password",
                                         password_confirmation: "password" } }
    end
    assert_equal 1, ActionMailer::Base.deliveries.size
    user = assigns(:user)
    assert_not user.activated?
    # Try to log in before activation.
    log_in_as(user)
    assert_not is_logged_in?
    # Invalid activation token
    get edit_account_activation_path("invalid token", email: user.email)
    assert_not is_logged_in?
    # Valid token, wrong email
    get edit_account_activation_path(user.activation_token, email: 'wrong')
    assert_not is_logged_in?
    # Valid activation token
    get edit_account_activation_path(user.activation_token, email: user.email)
    assert user.reload.activated?
    follow_redirect!
    assert_template 'users/show'
    assert is_logged_in?
  end
   
end

* I added activate and send_activation_email actions in User model.
sample_app/app/models/user.rb
  # 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

* I updated the action name for sending activationg email in Users Controller.
sample_app/app/controllers/users_controller.rb
@user.send_activation_email

* I updated (refactored) the codes to activate a user.
sample/app/controllers/account_activations_controller.rb
user.activate

* I simplified the two transactions into one transaction in User model.
sample_app/app/models/user.rb
update_columns(activated: true, activated_at: Time.zone.now)

* I edited the index action and show action to show only active users in Users Controller.
  def index
    @users = User.where(activated: true).paginate(page: params[:page])     
  end

  def show
    @user = User.find(params[:id])
    redirect_to root_url and return unless @user.activated?     
  end

* At the end of the chapter 11, I skipped the email in production using Heroku part. I ran a test, added all untracked files, committed the changes, merged back to Master branch, and pushed onto Github.
$ rails test
$ git add -A
$ git commit -m "Add account activation"
$ git checkout master
$ git merge account-activation




* The Github address for Michael Hartl's sample app project was: https://github.com/jimmy2046/sample_app

No comments:

Post a Comment

How to kill an abandoned process in Linux/Unix

I remembered it, then I forgot, then I remembered it, and then I forgot again. In case of a Linux/Unit process hang, I have to figure out ...