Supporting Guest Users in Rails 5 App

Starter Todo List App

Create a new Rails 5 app.

rails new clist

Create a user model.

rails g model user email handle password_digest

Create a task model.

rails g model task name complete:boolean user:references 

Open the migration file and make the default value for complete field as false.

class CreateTasks < ActiveRecord::Migration[5.0]
  def change
    create_table :tasks do |t|
      t.string :name
      t.boolean :complete, default: false
      t.references :user, foreign_key: true

      t.timestamps
    end
  end
end

Run the migration.

rails db:migrate

Add sample data in seeds.rb:

u = User.create! handle: "bbunny", email: "bugs@example.com", password: "secret"

u.tasks.create! name: "Meet Mr. Miyagi", complete: true
u.tasks.create! name: "Paint the fence", complete: true
u.tasks.create! name: "Wax the car"
u.tasks.create! name: "Sand the deck"

Populate the database:

rails db:seed

You will get the error:

ActiveModel::UnknownAttributeError: unknown attribute 'password' for User.

Add the has_secure_password to user model.

class User < ApplicationRecord
  has_secure_password
end

Uncomment the bcrypt gem in Gemfile.

gem 'bcrypt', '~> 3.1.7'

Run bundle. Populate the databse.

rails db:seed

You will get the error:

NoMethodError: undefined method `tasks' for #<User:0x007fa08>

Add the ActiveRecord declaration to user model:

has_many :tasks

Add the ActiveRecord declaration to task model:

belongs_to :user

Define the routes in routes.rb:

Rails.application.routes.draw do
  resources :users
  resources :tasks

  root to: 'tasks#index'
end

Create a users controller.

rails g controller users new

The implementation is straightforward:

class UsersController < ApplicationController
  def new
    @user = User.new
  end

  def create
    @user = User.new(allowed_params)
    if @user.save
      session[:user_id] = @user.id
      redirect_to root_url
    else
      render :new
    end
  end

  private

  def allowed_params
    params.require(:user).permit(:email, :handle, :password, :password_confirmation)
  end
end

Add current_user method to application controller.

class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception

  private

  def current_user
    @current_user ||= User.find(session[:user_id]) if session[:user_id]
  end

  helper_method :current_user
end

Create a tasks controller.

rails g controller tasks index

Implement the index, create, update and destroy.

class TasksController < ApplicationController
  def index
    if current_user
      @incomplete_tasks = current_user.tasks.where(complete: false)
      @complete_tasks = current_user.tasks.where(complete: true)
    else
      @incomplete_tasks = []
      @complete_tasks = []
    end
  end

  def create
    @task = current_user.tasks.create!(allowed_params)
    redirect_to tasks_url
  end

  def update
    @task = current_user.tasks.find(params[:id])
    @task.update_attributes!(allowed_params)
    respond_to do |f|
      f.html { redirect_to tasks_url }
      f.js
    end
  end

  def destroy
    @task = current_user.tasks.find(params[:id])
    @task.destroy
    respond_to do |f|
      f.html { redirect_to tasks_url }
      f.js
    end
  end

  private

  def allowed_params
    params.require(:task).permit(:name, :complete)
  end
end

Update the routes.rb:

Rails.application.routes.draw do
  get 'signup', to: 'users#new', as: 'signup'
  get 'login', to: 'sessions#new', as: 'login'
  get 'logout', to: 'sessions#destroy', as: 'logout'

  resources :users
  resources :sessions
  resources :tasks

  root to: 'tasks#index'
end

Generate the sessions controller to login and logout the user.

rails g controller sessions new 

Implement the create and destroy actions.

class SessionsController < ApplicationController
  def new
  end

  def create
    user = User.find_by(handle: params[:handle])
    if user && user.authenticate(params[:password])
      session[:user_id] = user.id
      redirect_to root_url, notice: 'Logged in!'
    else
      flash.now.alert = 'Name or password is invalid'
      render :new
    end
  end

  def destroy
    session[:user_id] = nil
    redirect_to root_url, notice: 'Logged out!'
  end
end

The sessions/new.html.erb looks like this:

<h1>Log In</h1>
<%= form_tag sessions_path do %>
  <div class="field">
    <%= label_tag :username %><br />
    <%= text_field_tag :handle, params[:handle] %>
  </div>
  <div class="field">
    <%= label_tag :password %><br />
    <%= password_field_tag :password %>
  </div>
  <div class="actions"><%= submit_tag "Log In" %></div>
<% end %>

The task partial in tasks/_task.html.erb looks like this:

<%= form_for task, remote: true do |f| %>
  <%= f.check_box :complete %>
  <%= f.label :complete, task.name %>
  <%= link_to "(remove)", task, method: :delete, data: {confirm: "Are you sure?"}, remote: true %>
<% end %>

You can now use the basic task management functionality, login, logout and signup.

Guest Record

In app/controllers/users_controller.rb, change the create action to add:

@user = params[:user] ? User.new(allowed_params) : User.new_guest
current_user.move_to(@user) if current_user && current_user.guest?

It will now look like this:

def create
  @user = params[:user] ? User.new(allowed_params) : User.new_guest
  if @user.save
    current_user.move_to(@user) if current_user && current_user.guest?
    session[:user_id] = @user.id
    redirect_to root_url
  else
    render :new
  end
end

In user model add the validation, guest record creation and moving data from guest to a new user:

class User < ApplicationRecord
  has_many :tasks, dependent: :destroy

  validates_presence_of :handle, :email, :password_digest
  validates_uniqueness_of :handle
  validates_confirmation_of :password

  has_secure_password

  def self.new_guest
    new(email: "#{Time.now}@bogus.com", 
        handle: Time.now,
        password_digest: 'bogus')
  end

  def move_to(user)
    tasks.update_all(user_id: user.id)
  end

  def guest?
    email.include?('bogus.com')
  end
end

The bogus guest records can be cleaned up by running a rake task that will delete old guest records. In app/views/layouts/application.html.erb display the links:

<% if current_user.guest? %>
  <%= link_to 'Become a member', signup_path %>
<% else %>
   Logged in as <strong><%= current_user.handle %></strong>
  <%= link_to "Log Out", logout_path %>
<% end %>

In app/views/tasks/index.html.erb change the code to:

<p><%= button_to "Try it for free!", users_path %></p>

You will now be able to try the app without creating a new account and become a member later. All the data will be transferred to the new account. You can download the source code from clist

Summary

In this article, you learned how to allow a visitor to try your web app without creating an account and how to transfer any data created for the guest user to the new account.


Related Articles


Create your own user feedback survey