Skip to content

tutorial rails prelaunch signup

Daniel Kehoe edited this page May 10, 2012 · 80 revisions

Rails Tutorial for a Startup Prelaunch Signup Site

by Daniel Kehoe

Last updated 9 May 2012

Ruby on Rails tutorial showing how to create a “beta launching soon” application for a startup prelaunch site with a signup page. You can clone the rails-prelaunch-signup repository for the complete example application.

Screenshot:

Rails Application for a Startup Prelaunch Signup Site

Read an interview with Michael Gajda of XPlaygrounds.com about how he used the rails-prelaunch-signup example to launch his startup site. If you launch a site using this example, add your link to the list of Rails Applications Built from the Examples.

Follow on Twitter Follow on Twitter

Follow the project on Twitter: @rails_apps. Tweet some praise if you like what you’ve found.

Introduction

The initial app for a typical web startup announces the founders’ plans and encourages visitors to enter an email address for future notification of the site’s launch. It’s not difficult to build such an app in Rails.

But why build it yourself if others have already done so? This project aims to:

  • eliminate effort spent building an application that meets a common need;
  • offer code that is already implemented and tested by a large community;
  • provide a well-thought-out app containing most of the features you’ll need.

By using code from this project, you’ll be able to:

  • direct your attention to the design and product offer for your prelaunch site;
  • get started faster building the ultimate application for your business.

The tutorial can help founders who are new to Ruby on Rails to learn how to build a real-world Rails web application.

Why We Use Devise

We’ve chosen to use the Devise gem for our authentication and user management. We use Devise because it offers a full set of features used in more complex applications, such as recovering a user’s forgotten password or allowing users to invite friends. By using Devise for the prelaunch signup application, you can use the same user database for your post-launch product. You’ll also have the benefit of receiving help from a large community of developers using Devise, should you need help in troubleshooting or customizing the implementation.

How We Configure Devise

Devise offers many features and configuration choices; in fact, exploring its options can take more time than actually implementing authentication from scratch. This app will save you time; we’ve selected the configuration that best accommodates a typical web startup signup site.

Devise handles signing up users through a registration feature. We use the Devise registration process to collect email addresses and create user accounts. Instead of confirming an email address immediately, we email a “thank you for your request” acknowledgment and postpone the email confirmation step. When you are ready to invite users to the site, the app provides an administrative interface so you can select users to receive an invitation that instructs them to confirm their email address and set a password.

The Devise wiki and answers on Stack Overflow can help you alter this implementation if you need different functionality.

RailsApps Examples and Tutorials

This is one in a series of Rails example apps and tutorials from the RailsApps Project.

This application is based on two of the RailsApps example apps:

The rails3-devise-rspec-cucumber app shows how to set up Devise for user authentication, explaining how to integrate RSpec and Cucumber for testing. The rails3-bootstrap-devise-cancan app shows how to set up Devise and add CanCan to manage access to administrative pages. You can use this tutorial without studying these example applications; if you find you are lost, it may be helpful to look at the two simpler examples.

Look at rails3-mongoid-devise example if you want to use the MongoDB datastore instead of ActiveRecord and a SQL database.

Tutorial Tutorial

This tutorial documents each step that you must follow to create this application. Every step is documented concisely, so a complete beginner can create this application without any additional knowledge. However, little explanation is offered for any of the steps, so if you are a beginner, you’re advised to look for an introduction to Rails elsewhere. See resources for getting started with Rails.

Most of the tutorials from the RailsApps project take about an hour to complete. This tutorial is more complex; it will take you about three hours to build the complete app. (Is our estimate accurate? Please leave a comment when you are done.)

Before You Start

If you follow this tutorial closely, you’ll have a working application that closely matches the example app in this GitHub repository. The example app is your reference implementation. If you find problems with the app you build from this tutorial, download the example app (in Git speak, clone it) and use a file compare tool to identify differences that may be causing errors. On a Mac, good file compare tools are FileMerge, DiffMerge, Kaleidoscope, or Ian Baird’s Changes.

If you clone and install the example app and find problems or wish to suggest improvements, please create a GitHub issue.

To improve this tutorial, please edit this wiki page or leave comments below.

Creating the Application

Option One

Follow this tutorial.

To create the application, you can cut and paste the code from the tutorial into your own files. It’s a bit tedious and error-prone but you’ll have a good opportunity to examine the code closely.

Option Two

Use the ready-made application template to generate the code.

We’ll soon offer an application template to generate a new Rails app with code that closely matches the tutorial. It’s not available yet.

Assumptions

Before beginning this tutorial, you need to install

  • The Ruby language (version 1.9.3)
  • Rails 3.2

Check that appropriate versions of Ruby and Rails are installed in your development environment:
$ ruby -v
$ rails -v

Be sure to read Installing Rails for detailed instructions and advice.

About the Software Development Process

Arguably, for a simple application like this, you don’t need a lot of ceremony. There’s no need for a written specification. And no need for tests, right? Just code it up. However, for the purposes of this tutorial, I want to show the practices that establish a good software development process. You may find it beneficial to use the same process when you develop more complex applications.

Here’s the software development process we’ll use:

  • write user stories
  • write a short specification for our feature set using Cucumber scenarios
  • create acceptance tests using Cucumber step definitions
  • code each feature
  • run acceptance tests as each feature is completed

By practicing the process that leads from concept to code, you’ll be prepared to build more complex applications.

Write Your User Stories

User stories are a way to discuss and describe the requirements for a software application. The process of writing user stories will help you identify all the features that are needed for your application. Breaking down the application’s functionality into discrete user stories will help you organize your work and track progress toward completion.

User stories are generally expressed in the following format:

As a <role>, I want <goal> so that <benefit>

As an example, here are four user stories we will implement for this application:

*Request Invitation*
As a visitor to the website
I want to request an invitation 
so I can be notified when the site is launched

*See Invitation Requests*
As the owner of the site
I want to view a list of visitors who have requested invitations
so I can know if my offer is popular

*Send Invitations*
As the owner of the site
I want to send invitations to visitors who have requested invitations
so users can try the site

*Collect Email Addresses*
As the owner of the site
I want to collect email addresses for a mailing list
so I can send announcements before I launch the site

If you have ideas for additional features for this application, simply create a GitHub issue describing the feature you’d like to see.

Create the Rails Application

Before you write any code, you’ll start by generating an example app using an application template script.

The $ character indicates a shell prompt; don’t include it when you run the command.

For a starter app using ActiveRecord and a SQL database, use the command:

$ rails new rails-prelaunch-signup -m https://raw.github.com/RailsApps/rails3-application-templates/master/rails3-bootstrap-devise-cancan-template.rb -T

Use the -T flags to skip Test::Unit files.

If you want to use the MongoDB datastore instead of ActiveRecord and a SQL database use the rails3-mongoid-devise starter app:

$ rails new rails-prelaunch-signup -m https://github.com/RailsApps/rails3-application-templates/raw/master/rails3-mongoid-devise-template.rb -T -O

Use the -T -O flags to skip Test::Unit files and Active Record files.

This creates a new Rails app named rails-prelaunch-signup on your computer. You can use a different name if you wish.

The application generator template will ask you for your preferences. For this tutorial, choose the following preferences:

  • Would you like to use Haml instead of ERB? yes
  • Would you like to use RSpec instead of TestUnit? yes
  • Would you like to use factory_girl for test fixtures with RSpec? yes
  • Would you like to use machinist for test fixtures with RSpec? no
  • Would you like to use Cucumber for your BDD? yes
  • Would you like to use Guard to automate your workflow? no
  • Would you like the app to use a Gmail account to send email? yes
  • Would you like to use Devise for authentication? #4
    1. No
    2. Devise with default modules
    3. Devise with Confirmable module
    4. Devise with Confirmable and Invitable modules
  • Would you like to manage authorization with CanCan & Rolify? yes
  • Which front-end framework would you like for HTML5 and CSS3? #4
    1. None
    2. Zurb Foundation
    3. Twitter Bootstrap (less)
    4. Twitter Bootstrap (sass)
    5. Skeleton
    6. Normalize CSS for consistent styling
  • Which form gem would you like? #3
    1. None
    2. simple form
    3. simple form (bootstrap)
  • Would you like to use rails-footnotes during development? no
  • Would you like to set a robots.txt file to ban spiders? yes
  • Would you like to add ‘will_paginate’ for pagination? no

Be sure to choose the CanCan & Rolify option as well as the Twitter Bootstrap (sass) option to create the example application.

Be sure to choose the Devise with Confirmable and Invitable modules option as it is required as a basis for the rails-prelaunch-signup app. You’ll also need the Bootstrap version of the SimpleForm gem.

You can choose other selections if you don’t care about matching the example application exactly.

After you create the application, switch to its folder to continue work directly in the application:

$ cd rails-prelaunch-signup

Edit the README

If your version of the app will be visible on GitHub, please edit the README file to add a description of the app and your contact info. Changing the README is important if you’re using a clone of the example app. I’ve been mistaken (and contacted) as the author of apps that are copied from my example.

Set Up Source Control (Git)

When you generate the starter app, the template sets up a source control repository and makes an initial commit of the code.

At this point, you should create a GitHub repository for your project.

See detailed instructions for Using Git with Rails.

Set Up Gems

It’s a good idea to create a new gemset using rvm, the Ruby Version Manager, as described in the article Installing Rails.

The starter app script sets up your gemfile.

Open your Gemfile and you should see the following. Gem version numbers may differ:

Gemfile

  source 'https://rubygems.org'
  gem 'rails', '3.2.3'
  gem 'sqlite3'
  group :assets do
    gem 'sass-rails',   '~> 3.2.3'
    gem 'coffee-rails', '~> 3.2.1'
    gem 'uglifier', '>= 1.0.3'
  end
  gem 'jquery-rails'
  gem "haml", ">= 3.1.4"
  gem "haml-rails", ">= 0.3.4", :group => :development
  gem "rspec-rails", ">= 2.9.0.rc2", :group => [:development, :test]
  gem "factory_girl_rails", ">= 3.2.0", :group => [:development, :test]
  gem "email_spec", ">= 1.2.1", :group => :test
  gem "cucumber-rails", ">= 1.3.0", :group => :test
  gem "capybara", ">= 1.1.2", :group => :test
  gem "database_cleaner", ">= 0.7.2", :group => :test
  gem "launchy", ">= 2.1.0", :group => :test
  gem "devise", ">= 2.1.0.rc"
  gem "devise_invitable", ">= 1.0.1"
  gem "cancan", ">= 1.6.7"
  gem "rolify", ">= 3.1.0"
  gem "bootstrap-sass", ">= 2.0.1"
  gem "simple_form"

Check for the current version of Rails and replace gem 'rails', '3.2.3' accordingly.

Note: The RailsApps examples are generated with application templates created by the Rails Apps Composer Gem. For that reason, groups such as :development or :test are specified inline. You can reformat the Gemfiles to organize groups in an eye-pleasing block style. The functionality is the same.

Install the Required Gems

When you add a new gem to the Gemfile, you should run the bundle install command to install the required gems on your computer. In this case, the starter app script has already run the bundle install command.

You can check which gems are installed on your computer with:

$ gem list --local

Keep in mind that you have installed these gems locally. When you deploy the app to another server, the same gems (and versions) must be available.

Haml

In this example, we’ll use Haml instead of the default “ERB” Rails template engine. The starter app script sets up Haml. You can see details about adding Haml to Rails with a discussion of the benefits and drawbacks in using Haml.

RSpec

The starter app script sets up RSpec for unit testing. Run rake -T to check that rake tasks for RSpec are available. You should be able to run rake spec to run all specs provided with the example app. To learn more about using RSpec, refer to The RSpec Book.

Cucumber

The starter app script sets up Cucumber for specifications and acceptance testing. To learn more about using Cucumber, refer to The Cucumber Book or the free introduction to Cucumber, The Secret Ninja Cucumber Scrolls.

You should be able to run rake cucumber, or more simply, cucumber, to run the Cucumber scenarios and steps provided with the example app. You can run a single Cucumber feature with a command such as:

$ bundle exec cucumber features/visitors/request_invitation.feature

The starter app script sets up the config/cucumber.yml file so it is not necessary to add --require features to the command to run a single Cucumber feature.

Configure Email

You must configure the app for your email account so your application can send email messages, for example, to acknowledge invitation requests or send welcome messages.

The starter app script sets up a default email configuration. You must add details about your email account.

Configure ActionMailer

ActionMailer is configured for development in the config/environments/development.rb file:

# ActionMailer Config
config.action_mailer.default_url_options = { :host => 'localhost:3000' }
config.action_mailer.delivery_method = :smtp
# change to false to prevent email from being sent during development
config.action_mailer.perform_deliveries = false
config.action_mailer.raise_delivery_errors = true
config.action_mailer.default :charset => "utf-8"

ActionMailer is configured for production in the config/environments/production.rb file:

config.action_mailer.default_url_options = { :host => 'example.com' }
# ActionMailer Config
# Setup for production - deliveries, no errors raised
config.action_mailer.delivery_method = :smtp
config.action_mailer.perform_deliveries = true
config.action_mailer.raise_delivery_errors = false
config.action_mailer.default :charset => "utf-8"

ActionMailer is configured for testing in the config/environments/test.rb file:

# ActionMailer Config
config.action_mailer.default_url_options = { :host => 'example.com' }

This will set the example application to deliver email in production. Email messages are visible in the log file so there is no need to send email in development. The configuration above will raise delivery errors in development but not in production.

In development, config.action_mailer.default_url_options is set for a host at localhost:3000 which will enable links in Devise confirmation email messages to work properly during development.

For testing, config.action_mailer.default_url_options is set for a host at example.com. Any value allows tests to run.

For production, you’ll need to change the config.action_mailer.default_url_options host option from example.com to your own domain.

Use a Gmail account

If you want to use a Gmail account to send email, you’ll need to modify the files config/environments/development.rb and config/environments/production.rb:

config.action_mailer.smtp_settings = {
  address: "smtp.gmail.com",
  port: 587,
  domain: "example.com",
  authentication: "plain",
  enable_starttls_auto: true,
  user_name: ENV["GMAIL_USERNAME"],
  password: ENV["GMAIL_PASSWORD"]
}

You can replace ENV["GMAIL_USERNAME"] and ENV["GMAIL_PASSWORD"] with your Gmail username and password. However, committing the file to a public GitHub repository will expose your secret password.

If you’re familiar with setting Unix environment variables, it’s advisable to leave config.action_mailer.smtp_settings unchanged and set your environment variables in the file that is read when starting an interactive shell (the ~/.bashrc file for the bash shell). This will keep the password out of your repository.

Are you using a bash shell? Use echo $SHELL to find out. For a bash shell, edit the ~/.bashrc file and add:

export GMAIL_USERNAME="[email protected]"
export GMAIL_PASSWORD="secret*"

Open a new shell or restart your terminal application to continue.

Configure Devise for Email

Complete your email configuration by modifying

config/initializers/devise.rb

and setting the config.mailer_sender option for the return email address for messages that Devise sends from the application.

Set Up the Database

Create a Default User

You’ll want to set up a default user so you can test the app. The file db/seeds.rb already contains:

puts 'SETTING UP DEFAULT USER LOGIN'
user = User.create! :name => 'First User', :email => '[email protected]', :password => 'please', :password_confirmation => 'please', :confirmed_at => Time.now.utc
puts 'New user created: ' << user.name
user2 = User.create! :name => 'Second User', :email => '[email protected]', :password => 'please', :password_confirmation => 'please', :confirmed_at => Time.now.utc
puts 'New user created: ' << user2.name
user.add_role :admin

You can change the values for name, email, and password as you wish.

Seed the Database

The starter app script has already set up the database and added the default user.

If you change the default user’s name, email, or password you’ll need to reset the database:

$ bundle exec rake db:reset

If you need to, you can run $ bundle exec rake db:reset whenever you need to recreate the database.

Test the Starter App

You can check that the example app runs properly by entering the command

$ rails server

To see your application in action, open a browser window and navigate to http://localhost:3000/. You should see the default user listed on the home page. When you click on the user’s name, you should be required to log in before seeing the user’s detail page.

Stop the server with Control-C.

Modify the Starter App

If you’ve tested the example app, you’ve seen that any user who logs in will see a list of all the users on the home page. That’s fine for an example app but it’s not what we want for production.

Update the Home Page

Replace the contents of the file app/views/home/index.html.haml:

%h3 Welcome

You can embellish the page as you wish.

Modify the file app/controllers/home_controller.rb to remove the index method:

class HomeController < ApplicationController
end

Create an Initializer File

We will set a configuration constant Rails.configuration.launched in an initializer file. This constant will be set to false (before we launch our site) or true (after we launch our site).

Create a file config/initializers/prelaunch-signup.rb:

# change to "true" (and restart) when you want visitors to sign up without an invitation
Rails.configuration.launched = false

We’ll use the configuration constant anywhere in the application where we want behavior to be dependent on whether we’ve launched the site.

Feature: Request Invitation

Now we’ll pick a user story and turn it into a specification that will guide implementation of a feature.

First we’ll get our git workflow set up for adding a new feature.

Git Workflow

When you are using git for version control, you can commit every time you save a file, even for the tiniest typo fixes. If only you will ever see your git commits, no one will care. But if you are working on a team, either commercially or as part of an open source project, you will drive your fellow programmers crazy if they try to follow your work and see such “granular” commits. Instead, get in the habit of creating a git branch each time you begin work to implement a feature. When your new feature is complete, merge the branch and “squash” the commits so your comrades see just one commit for the entire feature.

Create a new git branch for this feature:

$ git checkout -b request-invitation

The command creates a new branch named “request-invitation” and switches to it, analogous to copying all your files to a new directory and moving to work in the new directory (though that is not really what happens with git).

User Story

Here’s the user story we’ll specify and implement:

*Request Invitation*
As a visitor to the website
I want to request an invitation 
so I'll be notified when the site is launched

In many situations, your user story is all you need to guide you in coding the implementation. If you are both the product owner and a hands-on developer, you don’t need a written specification to implement a feature. Many developers code directly from a user story. However, I recommend wriitng a specification before coding.

Consider the benefits of writing a specification:

  • It helps us discover the functionality we need to implement.
  • It is a “to-do list” to identify what needs to be accomplished and helps us track progress.
  • It helps us describe and discuss features with our business partners.
  • It is the basis for acceptance testing or integration testing.

For the purposes of this tutorial, acceptance tests and integration tests are synonymous. Acceptance tests demonstrate that developers have succeeded in implementing a specification. Integration tests assure you that your application runs as intended. We will use the specification to create an automated acceptance test so we know when a feature has been successfully implemented. Our acceptance tests also serve as integration tests so we can continue to test the code as we build out or maintain the application. Automated acceptance tests are the key to managing risk for a software development project.

We’ll use Cucumber to create our specification and acceptance tests. Not all developers use Cucumber. Some developers create integration tests using Capybara in combination with RSpec as described in Ryan Bates’s How I Test Railscast. Cucumber is appropriate when a team includes nonprogrammers who are involved in defining product requirements or there is a need for a specification and acceptance tests to be maintained independently of implementation (for example, when implementation is outsourced). For this tutorial, we may all be programmers, but using Cucumber to create a specification helps to describe the features, organize the tutorial, and break the work into discrete tasks.

Cucumber is a tool for managing software development. It’s up to you to decide if you need to establish management processes at this stage of your business.

Cucumber Scenario

Now we begin writing our specification.

The features directory contains our Cucumber feature files. We can organize Cucumber feature files any way we wish by placing them in subdirectories. For this application, we’ll organize features by roles in subdirectories for “visitors” and “owner”. Create a subdirectory features/visitors and then create the following file:

features/visitors/request_invitation.feature

Feature: Request Invitation
  As a visitor to the website
  I want to request an invitation 
  so I can be notified when the site is launched

  Background:
    Given I am not logged in

  Scenario: User views home page
    When I visit the home page
    Then I should see a button "Request Invitation"

  Scenario: User views invitation request form
    When I visit the home page
    And I click a button "Request Invitation"
    Then I should see a form with a field "Email"

  Scenario: User signs up with valid data
    When I request an invitation with valid user data
    Then I should see a message "Thank You"
    And my email address should be stored in the database
    And my account should be unconfirmed
    And I should receive an email with subject "Request Received"

  Scenario: User signs up with invalid email
    When I request an invitation with an invalid email
    Then I should see an invalid email message

This Cucumber feature file contains the specification needed to implement the user story “Request Invitation.”

It’s important to note that user stories don’t necessarily translate directly into Cucumber features. In this case, our first user story is easily transformed into a set of Cucumber scenarios that describe the user story completely. This is not always the case; don’t be concerned if Features != User Stories as explained in Matt Wynne’s blog post.

Cucumber Step Definitions

Here we turn our specification into an automated acceptance test.

Cucumber scenarios can be read as plain English text. Alone, they serve as specifications. To create an acceptance test or integration test, we must write test code for each step in a scenario, called “step definitions.” Our test code interacts directly with our application, as if it was a visitor to the website using the application.

We’ll create step definitions for all the scenario steps in our “Feature: Request Invitation” file.

Create the following file:

features/step_definitions/visitor_steps.rb

def new_user
  @user ||= { :email => "[email protected]",
    :password => "please", :password_confirmation => "please" }
end

def invitation_request user
  visit '/users/sign_up'
  fill_in "Email", :with => user[:email]
  click_button "Request Invitation"
end

When /^I visit the home page$/ do
    visit root_path
end

Then /^I should see a button "([^\"]*)"$/ do |arg1|
  page.should have_button (arg1)
end

When /^I click a button "([^"]*)"$/ do |arg1|
  click_button (arg1)
end

Then /^I should see a form with a field "([^"]*)"$/ do |arg1|
  page.should have_content (arg1)
end

Then /^I should see a message "([^\"]*)"$/ do |arg1|
  page.should have_content (arg1)
end

Then /^my email address should be stored in the database$/ do
  test_user = User.find_by_email("[email protected]")
  test_user.should respond_to(:email)
end

Then /^my account should be unconfirmed$/ do
  test_user = User.find_by_email("[email protected]")
  test_user.confirmed_at.should be_nil
end

When /^I request an invitation with valid user data$/ do
  invitation_request new_user
end

When /^I request an invitation with an invalid email$/ do
  user = new_user.merge(:email => "notanemail")
  invitation_request user
end

These step definitions accommodate all scenarios in our “Request Invitation” feature.

Cucumber uses all the step definitions in separate files in the features/step_definitions/ directory so we can group the step definitions in as many files as we want.

Be sure you’ve set up the database for testing before running Cucumber:

$ bundle exec rake db:test:prepare

Then we can run our integration test with the following command:

$ bundle exec cucumber features/visitors/request_invitation.feature

The test should fail indicating that the home page has no button “Request Invitation.”

Implement “Request Invitation” Form

We begin implementing the actual application here.

The application’s home page doesn’t contain a “Request Invitation” form. Should we add a sign-up form to the home page? We could but we already have a sign-up form provided by Devise, our authentication gem. It’ll be easier to adapt the existing Devise mechanism.

We’ll modify the Devise form to use the SimpleForm gem. The SimpleForm gem lets us create forms that include tags that apply attractive Twitter Bootstrap form styles.

Take a look at the file app/views/devise/registrations/new.html.haml. We’ll modify it to make it a “Request Invitation” form.

  • We’ll change the heading from “Sign up” to “Request Invitation.”
  • We’ll remove the password fields.
  • We’ll remove the user name field (you could keep it if you wish).
  • We’ll change the submit button text from “Sign up” to “Request Invitation.”
%h2 Request Invitation
= simple_form_for(resource, :as => resource_name, :url => registration_path(resource_name)) do |f|
  = devise_error_messages!
  = f.input :email, :placeholder => '[email protected]'
  = f.submit "Request Invitation"

Override Password Validation

Devise won’t let us create a new user without a password when we use the default :validatable module. We want Devise to validate the email address but ignore the missing password, so we’ll override Devise’s password_required? method.

Modify the file app/models/user.rb to override methods supplied by Devise:

class User < ActiveRecord::Base
  rolify
  # Include default devise modules. Others available are:
  # :token_authenticatable, :encryptable, :confirmable, :lockable, :timeoutable and :omniauthable
  devise :invitable, :database_authenticatable, :registerable, :confirmable,
         :recoverable, :rememberable, :trackable, :validatable

  # Setup accessible (or protected) attributes for your model
  attr_accessible :name, :email, :password, :password_confirmation, :remember_me, :confirmed_at

  # override Devise method
  # no password is required when the account is created; validate password when the user sets one
  def password_required?
    if !persisted? 
      false
    else
      !password.nil? || !password_confirmation.nil?
    end
  end
  
end

Adjust Acceptance Tests

To make sure our acceptance tests continue to pass, we’ll need to make changes to the Cucumber step definition file features/step_definitions/user_steps.rb.

Replace the sign_up method with the following:

def sign_up
  delete_user
  visit '/users/sign_up'
  fill_in "Email", :with => @visitor[:email]
  click_button "Request Invitation"
  find_user
end

We no longer ask the visitor to set a password when the account is created.

In the features/users/sign_up.feature file, remove the following:

Scenario: User signs up without password
  When I sign up without a password
  Then I should see a missing password message

Scenario: User signs up without password confirmation
  When I sign up without a password confirmation
  Then I should see a missing password confirmation message

Scenario: User signs up with mismatched password and confirmation
  When I sign up with a mismatched password confirmation
  Then I should see a mismatched password message

We no longer ask the visitor to supply a name when the account is created.

In the features/users/user_show.feature file, remove the following:

Scenario: Viewing users
  Given I exist as a user
  When I look at the list of users
  Then I should see my name

Use Devise Registrations Page for the Home Page

Examine the config/routes.rb file to learn how to use the Devise registrations page as our home page.

The starter app script sets up the config/routes.rb file with a route to the home page for authenticated users (those who have an account and are logged in) and the same route for all other users (those who have no account or are not logged in).

authenticated :user do
  root :to => 'home#index'
end

root :to => "home#index"

To make the Devise registrations page serve as the home page for users who have no account (or are not logged in), replace the second root :to => "home#index" so you see this:

authenticated :user do
  root :to => 'home#index'
end
  
devise_scope :user do
  root :to => "devise/registrations#new"
end

With this change, casual visitors will see a “Request Invitation” form on the home page. And users who log in (presumably only an administrator or invited guests) will see the application “home#index” page.

Create a “Thank You” Page

For a simple “thank you” page, there’s no need to create a dynamic page with a controller and view. Instead, create a file for a static web page:

public/thankyou.html

<h1>Thank You</h1>

Obviously, you can embellish this page as needed.

Later, this tutorial will show you how to eliminate the “thank you” page and use AJAX to update the sign up page with a thank you message. For now, it’s helpful to see a simple implementation without AJAX.

Redirect to “Thank You” Page After Successful Sign Up

We want the visitor to see the “thank you” page after they request an invitation.

Override the Devise::RegistrationsController with a new controller. Create a file:

app/controllers/registrations_controller.rb

class RegistrationsController < Devise::RegistrationsController
  protected

  def after_inactive_sign_up_path_for(resource)
    '/thankyou.html'
  end
  
  def after_sign_up_path_for(resource)
    '/thankyou.html'
  end
  
end

When a visitor creates an account by requesting an invitation, Devise will call the after_inactive_sign_up_path_for method to redirect the visitor to the thank you page. In the future, after you launch the application and allow visitors to become active as soon as they create an account, you may use the after_sign_up_path_for to redirect the new user to the thank you page.

Modify config/routes.rb to use the new controller. Replace devise_for :users with:

devise_for :users, :controllers => { :registrations => "registrations" }

To make sure our acceptance tests continue to pass, we’ll need to make changes to the Cucumber step definition file features/step_definitions/user_steps.rb.

Replace the “successful sign up message” step definition content with “Thank You” (or anything else you’ve added to the thank you page):

Then /^I should see a successful sign up message$/ do
  page.should have_content "Thank You"
end

For more information, the Devise wiki explains How to Redirect to a Specific Page on Successful Sign Up.

Postpone Confirmation of New Accounts

We want a visitor to create a new account when they request an invitation but we don’t want to confirm the email address and activate the account until we send an invitation.

There are several ways to implement this functionality. One approach is to add an “active” attribute to the User model and designate the account as “inactive” when it is created. Another approach is to simply revise the confirmation email to make it a simple “welcome” without the confirmation request but this would require re-implementing the confirmation request process later. The simplest approach is to postpone sending the user a request to confirm their email address, leaving the account unconfirmed until after we send the user an invitation.

We’ll modify the file app/models/user.rb to override methods supplied by Devise:

class User < ActiveRecord::Base
  rolify
  # Include default devise modules. Others available are:
  # :token_authenticatable, :encryptable, :confirmable, :lockable, :timeoutable and :omniauthable
  devise :invitable, :database_authenticatable, :registerable, :confirmable,
         :recoverable, :rememberable, :trackable, :validatable

  # Setup accessible (or protected) attributes for your model
  attr_accessible :name, :email, :password, :password_confirmation, :remember_me, :confirmed_at

  # override Devise method
  # no password is required when the account is created; validate password when the user sets one
  def password_required?
    if !persisted? 
      false
    else
      !password.nil? || !password_confirmation.nil?
    end
  end
  
  # override Devise method
  def confirmation_required?
    false
  end
  
  # override Devise method
  def active_for_authentication?
    confirmed? || confirmation_period_valid?
  end

end

Devise uses a conditional “after_create” callback to generate a confirmation token and send the confimation request email. It is only called if confirmation_required? returns true. By indicating that “confirmation is not required,” no confimation email will be sent when the account is created.

When we tell Devise that “confirmation is not required,” Devise will assume that any new account is “active_for_authentication.” We don’t want that, so we override the active_for_authentication? method so that unconfirmed accounts are not active.

Send a Welcome Email

Ordinarily, when the Devise Confirmable module is configured and a new user requests an invitation, Devise will send an email with instructions to confirm the account. We’ve changed this behavior so the user doesn’t get a confirmation request. However, we still want to send a welcome email.

We didn’t revise the confirmation email to make it a welcome message. That might seem simpler but it would require us to re-implement the confirmation request process later. Instead we’ll add send an email welcome message using a new ActionMailer method when the account is created.

Generate a mailer with views and a spec:

$ rails generate mailer UserMailer

Modify the file spec/mailers/user_mailer_spec.rb to create a test:

require "spec_helper"

describe UserMailer do
  before(:all) do
    @user = FactoryGirl.create(:user, email: "[email protected]")
    @email = UserMailer.welcome_email(@user).deliver
  end

  it "should be delivered to the email address provided" do
    @email.should deliver_to("[email protected]")
  end

  it "should contain the correct message in the mail body" do
    @email.should have_body_text(/Welcome/)
  end

  it "should have the correct subject" do
    @email.should have_subject(/Request Received/)
  end

end

Add a welcome_email method to the mailer by editing the file app/mailers/user_mailer.rb:

class UserMailer < ActionMailer::Base
  default :from => "[email protected]"

  def welcome_email(user)
    mail(:to => user.email, :subject => "Invitation Request Received")
  end
end

Create a mailer view by creating a file app/views/user_mailer/welcome_email.html.erb. This will be the template used for the email, formatted in HTML:

<!DOCTYPE html>
<html>
  <head>
    <meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
  </head>
  <body>
    <h1>Welcome</h1>
    <p>
      We have received your request for an invitation to example.com.
    </p>
    <p>
      We'll contact you when we launch.
    </p>
  </body>
</html>

It is a good idea to make a text-only version for this message. Create a file app/views/user_mailer/welcome_email.text.erb:

Welcome!

We have received your request for an invitation to example.com.
 
We'll contact you when we launch.

When you call the mailer method, ActionMailer will detect the two templates (text and HTML) and automatically generate a multipart/alternative email.

Now we’ll wire up the User model to send the welcome message when an account is created.

Modify the file app/models/user.rb to add the send_welcome_email method:

class User < ActiveRecord::Base
  rolify
  # Include default devise modules. Others available are:
  # :token_authenticatable, :encryptable, :confirmable, :lockable, :timeoutable and :omniauthable
  devise :invitable, :database_authenticatable, :registerable, :confirmable,
         :recoverable, :rememberable, :trackable, :validatable

  # Setup accessible (or protected) attributes for your model
  attr_accessible :name, :email, :password, :password_confirmation, :remember_me, :confirmed_at

  after_create :send_welcome_email

  # override Devise method
  def confirmation_required?
    false
  end
  
  # override Devise method
  def active_for_authentication?
    confirmed? || confirmation_period_valid?
  end
  
  private

  def send_welcome_email
    UserMailer.welcome_email(self).deliver
  end

end

Now the visitor will get a welcome email when they request an invitation.

Tweak the User Interface

The required functionality is largely complete. But there are a few small changes to make.

If the user visits the sign-in page immediately after requesting an invitation, they may see this flash message:

“A message with a confirmation link has been sent to your email address. Please open the link to activate your account.”

In the file config/locales/devise.en.yml, find this message:

signed_up_but_unconfirmed: 'A message with a confirmation link has been sent to your email address. Please open the link to activate your account.'

Replace it with this:

signed_up_but_unconfirmed: 'Your invitation request has been received. You will receive an invitation when we launch.'

Be careful not to use an apostrophe or single quote in the message unless you surround the text with double quotes.

If the visitor attempts to sign in, they will see this message:

“You have to confirm your account before continuing.”

In the file config/locales/devise.en.yml, find this message:

unconfirmed: 'You have to confirm your account before continuing.'

Replace it with this:

unconfirmed: 'Your account is not active.'

To make sure our acceptance tests continue to pass, we’ll need to make changes to the Cucumber step definition file features/step_definitions/user_steps.rb.

Replace the “unconfirmed account message” step definition:

Then /^I see an unconfirmed account message$/ do
  page.should have_content "Your account is not active."
end

The user will also see links on the sign-in page: “Didn’t receive confirmation instructions?” and “Forgot your password?”. We want visitors to see these links after we launch. Before we launch, we don’t want visitors to see these links. The sign-in page is provided from a view template in the Devise gem. We don’t have to modify the sign-in page. Instead we’ll create a new “links” partial.

In the “links” partial we’ll use a configuration constant: Rails.configuration.launched. This constant is set to false (before we launch our site) or true (after we launch our site). When the constant is false, the links for “Didn’t receive confirmation instructions?” and “Forgot your password?” will not appear.

Create a file app/views/devise/shared/_links.html.haml:

- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks'
  = link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name)
  %br/
- if devise_mapping.omniauthable?
  - resource_class.omniauth_providers.each do |provider|
    = link_to "Sign in with #{provider.to_s.titleize}", omniauth_authorize_path(resource_name, provider)
    %br/
- if devise_mapping.confirmable? && controller_name != 'confirmations' && Rails.configuration.launched == true
  = link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name)
  %br/
- if devise_mapping.recoverable? && controller_name != 'passwords' && Rails.configuration.launched == true
  = link_to "Forgot your password?", new_password_path(resource_name)
  %br/

Now our feature is complete.

Test the Implemention of the “Request Invitation” Feature

Run the integration test with the following command:

$ bundle exec cucumber features/visitors/request_invitation.feature

The test should succeed.

Evaluating only the functionality, the feature is complete. In the next section, we’ll improve the look and feel of the feature.

Git Workflow

Since the new feature is complete, merge the working branch to “master” and squash the commits so you have just one commit for the entire feature:

$ git checkout master
$ git merge --squash request-invitation
$ git commit -am "implement 'Request Invitation' feature"

You can delete the working branch when you’re done:

$ git branch -D request-invitation

Improving the Design: Modal Window

The functionality of the “Request Invitation” feature is complete. Let’s improve the look and feel of the feature.

Git Workflow

Create a new git branch for the changes you’ll make to the page design:

$ git checkout -b work-in-progress

The command creates a new branch named “work-in-progress” and switches to it.

Add a Modal Window

It’d be nice for the “request invitation” page to include some “romance copy” to convince the visitor of the value of the site. The “call to action” could be a big button that says, “Request Invitation,” which opens a modal window and invites the visitor to enter an email address and click a submit button.

Twitter Bootstrap gives us everything we need to implement this in a few lines of code.

Open the file app/views/devise/registrations/new.html.haml and replace the contents with this new code:

#request-invite.modal{:style => "display: none;"}
  = simple_form_for resource, :as => resource_name, :url => registration_path(resource_name) , :html => {:class => 'form-horizontal' } do |f|
    .modal-header
      %a.close{"data-dismiss" => "modal"} ×
      %h3 Request Invitation
    .modal-body
      = f.error_notification
      = f.hidden_field :password, :value => 'please'
      = f.input :email, :placeholder => '[email protected]'
    .modal-footer
      = f.submit "Request Invitation", :class => "btn.btn-success", :id => 'invitation_button'
      %a.btn{"data-dismiss" => "modal", :href => "#"} Close
#romance-copy{:style => "text-align: center; margin-top: 80px"}
  %h2 Want in?
#call-to-action{:style => "text-align: center; margin-top: 80px"}
  %a.btn.btn-primary.btn-large{"data-toggle" => "modal", :href => "#request-invite"} Request invite

The revised view code includes CSS classes from Twitter Bootstrap that implement the modal window and apply style to the buttons. You’ll note that our code includes some simple style rules to position the romance copy and the call-to-action button. These styles could be moved to an external stylesheet; for now, it’s easier to include them in the view file.

Display Errors

Displaying the “request invitation” form inside a modal window is a nice touch but it creates a problem: Form validation error messages are hidden. See for yourself. Open the modal window, leave the email field blank and click the submit button. You’ll see the home page without any indication that there was a problem. Click the home page “Request Invite” button again and you’ll see there is an error message in the modal window.

Our first option is to force the form to be displayed when errors are present. We can do this by modifying the first line like this:

#request-invite.modal{:style => "display: #{@user.errors.any? ? 'block' : 'none'};"}

When you test this by submitting a form without an email address, you’ll see the form appears with error messages. However, we don’t see the full effect of the modal window because the page is not opaque as it should be with the modal window. We can keep the modified code, but we really need some jQuery code to trigger the modal window when an error message is present.

Add the following to the file app/assets/javascripts/application.js:

// display validation errors for the "request invitation" form
$('document').ready(function() {
  if ($('#error_explanation').length > 0) {
    $("#request-invite").modal('toggle');
  }
})

When the #error_explanation element contains text, the page will refresh and the modal window will be displayed properly.

Improving the Design: Adding AJAX

Thank you to Andrea Pavoni for the AJAX implementation.

We’ve improved the appearance of the “invitation request” page by adding a modal window using Twitter Bootstrap. We can make further improvements. As implemented, the page refreshes with a roundtrip to the server when we submit the form successfully (or get an an error). Our app will look more sophisticated if we use AJAX techniques to update the page without a page refresh.

We don’t need changes to the “invitation request” page. We’ll add a partial to provide a “thank you” message. Add a file app/views/devise/registrations/_thankyou.html.haml:

%h1 Thank you
#request-invite.modal{:style => "display: 'block';"}
  .modal-header
    %a.close{"data-dismiss" => "modal"} ×
    %h3 Thank you!
  .modal-body
    %p We have received your request for an invitation to example.com.
    %p We'll contact you when we launch.

Next, add some JavaScript to the file app/assets/javascripts/application.js. This will trigger an AJAX submission action when the “request invitation” button is clicked and update the #request-invite div on completion.

$('document').ready(function() {
  
  // display validation errors for the "request invitation" form
  if ($('#error_explanation').length > 0) {
    $("#request-invite").modal('toggle');
  }
  
  // use AJAX to submit the "request invitation" form
  $('#invitation_button').live('click', function() {
    var email = $('form #user_email').val();
    var password = $('form #user_password').val();
    var dataString = 'user[email]='+ email + '&user[password]=' + password;
    $.ajax({
      type: "POST",
      url: "/users",
      data: dataString,
      success: function(data) {
        $('#request-invite').html(data);
      }
    });
    return false;
  });
  
})

Finally, we need to override the create method in the Devise registration controller to render the “thank you” partial. Modify the file app/controllers/registrations_controller.rb:

class RegistrationsController < Devise::RegistrationsController

  # ovverride #create to respond to AJAX with a partial
  def create
    build_resource

    if resource.save
      if resource.active_for_authentication?
        sign_in(resource_name, resource)
        (render(:partial => 'thankyou', :layout => false) && return)  if request.xhr?
        respond_with resource, :location => after_sign_up_path_for(resource)
      else
        expire_session_data_after_sign_in!
        (render(:partial => 'thankyou', :layout => false) && return)  if request.xhr?
        respond_with resource, :location => after_inactive_sign_up_path_for(resource)
      end
    else
      clean_up_passwords resource
      render :action => :new, :layout => !request.xhr?
    end
  end

  protected

  def after_inactive_sign_up_path_for(resource)
    '/thankyou.html'
  end

  def after_sign_up_path_for(resource)
    '/thankyou.html'
  end

end

To make sure our acceptance tests continue to pass, we’ll need to make a tweak to the Cucumber step definition file features/step_definitions/user_steps.rb.

Replace the “unconfirmed account message” step definition to accommodate the SimpleForm output:

Then /^I should see an invalid email message$/ do
  page.should have_content "Emailis invalid"
end

Now we can say we have built a “Web 2.0” application. When the visitor submits the form, the modal window changes to display a “thank you” message (or an error message) without a page refresh.

Git Workflow

Merge the working branch to “master”:

$ git checkout master
$ git merge --squash work-in-progress
$ git commit -am "improve design for 'Request Invitation' feature"
$ git branch -D work-in-progress

Feature: View Progress

Now we’ll implement the next user story. As the owner of the site, you’ll want to visit and see how many visitors have requested invitations. You likely don’t want anyone else to view that information, so we’ll need some form of authorization to restrict access to yourself or approved administrators.

First we’ll set up our git workflow so we can add a new feature.

Git Workflow

Create a new git branch for this feature:

$ git checkout -b view-progress

The command creates a new branch named “view-progress.”

User Story

Here’s the user story we’ll specify and implement:

*View Progress*
As the owner of the site
I want to know how many visitors have requested invitations
so I can know if my offer is popular

Cucumber Scenario

Let’s write our specification.

Create a subdirectory features/admin and then create the following file:

features/admin/view_progress.feature

Feature: View Progress
  As the owner of the site
  I want to know how many visitors have requested invitations
  so I can know if my offer is popular

  Scenario: Administrator views list of users
    Given I am logged in as an administrator
    When I visit the users page
    Then I should see a list of users

  Scenario: User cannot view list of users
    Given I am logged in
    When I visit the users page
    Then I should see an access denied message

This Cucumber feature file contains the specification needed to implement the user story “View Progress.”

Cucumber Step Definitions

Turn the specification into an automated acceptance test. Create step definitions for all the scenario steps in our “Feature: View Progress” file.

Create the following file:

features/step_definitions/admin_steps.rb

Given /^I am logged in as an administrator$/ do
  create_user
  @user.add_role :admin
  sign_in
end

When /^I visit the users page$/ do
  visit users_path
end

Then /^I should see a list of users$/ do
  page.should have_content @user[:email]
end

Then /^I should see an access denied message$/ do
  page.should have_content "Not authorized as an administrator"
end

Be sure you’ve set up the database for testing before running Cucumber:

$ bundle exec rake db:test:prepare

Then we can run our integration test with the following command:

$ bundle exec cucumber features/admin/view_progress.feature

The test will succeed. That’s because the application template we used to generate the application already implements the functionality we want. Let’s see how.

The Administrative Page

We already have a page that shows a list of users.

Take a look at the file app/views/users/index.html.haml:

%h2 Users
- @users.each do |user|
  %br/
  #{link_to user.email, user} signed up #{user.created_at.to_date}

We can use this page as our “administrative dashboard.” It’s fine for our example application; if you later build a more complex application, you may wish to create an AdminController with corresponding Admin views.

Restricting Access to the Administrative Page

Take a look at the controller file app/controllers/users_controller.rb:

class UsersController < ApplicationController
  before_filter :authenticate_user!

  def index
    authorize! :index, @user, :message => 'Not authorized as an administrator.'
    @users = User.all
  end
  
  def show
    @user = User.find(params[:id])
  end

end

Notice the index method contains a statement which limits access to administrators only:

authorize! :index, @user, :message => 'Not authorized as an administrator.'

When we generated our starter app, we answered “yes” to the question, “Would you like to manage authorization with CanCan & Rolify?” The application template set up everything we need for limiting access to administrative pages. You can take a look at the tutorial for the rails3-bootstrap-devise-cancan example app to see how it is done. In a nutshell, Rolify allows us to designate a user as an “admin.” CanCan provides an Ability class and the authorize! method. We add the authorize! method to any controller action that should be restricted to administrators only. The authorize! method makes a call to the Ability class to determine which users are authorized for access.

If you look at the file app/models/ability.rb, you’ll see this:

class Ability
  include CanCan::Ability

  def initialize(user)
    user ||= User.new # guest user (not logged in)
    if user.has_role? :admin
      can :manage, :all
    end
  end
  
end

Very simply, if we add the authorize! method to any controller action, it will check the Ability class and allow access for users in the “admin” role. By default, other users will not be allowed. Of course, if the authorize! method is not included in a controller action, any user will have access.

Test the Implemention of the “View Progress” Feature

Run the integration test with the following command:

$ bundle exec cucumber features/admin/view_progress.feature

The test should succeed.

Git Workflow

Since the new feature is complete, merge the working branch to “master” and squash the commits so you have just one commit for the entire feature:

$ git checkout master
$ git merge --squash view-progress
$ git commit -am "implement 'View Progress' feature"

You can delete the working branch when you’re done:

$ git branch -D view-progress

Improving the Design: Adding a Chart

Our “administrative dashboard” shows a list of visitors who have requested invitations. It meets our basic requirements but I’m sure we’d prefer to see the trend of sign-ups over time. Let’s add a chart.

If you look at The Ruby Toolbox site in the Graphing category, you’ll see ten gems of varying popularity, age and activity. Google offers an online service, Google Chart Tools, that uses Javascript in the browser to generate charts and graphs. Two of the gems listed use Google Chart Tools to generate charts: Matt Aimonetti’s GoogleCharts and Winston Teo’s GoogleVisualr. We’ll use the GoogleVisualr gem; it includes a useful view helper.

First we’ll set up our git workflow so we can add a new feature.

Git Workflow

Create a new git branch for this feature:

$ git checkout -b add-chart

GoogleVisualr Gem

We need to add the GoogleVisualr gem to the Gemfile:

gem "google_visualr", ">= 2.1.2"

Run the bundle install command to install the required gem on your computer:

$ bundle install

Javascript for Google Chart Tools

We’ll add the Javascript library that loads the Google Chart Tools API. We won’t add it to the Javascript files in our asset pipeline. Instead, we’ll add it to the application layout for the administrative page, loading it directly from Google’s content delivery network (CDN).

We want to add it to a single page of our application. Our default application layout will accept a page-specific addition to the head section. The = yield(:head) statement in the file app/views/layouts/application.html.haml will incorporate anything we add to a page with the content_for :head tag.

Modify the file app/views/users/index.html.haml:

- content_for :head do
  = javascript_include_tag 'http://www.google.com/jsapi'

%h2 Users
- @users.each do |user|
  %br/
  #{link_to user.email, user} signed up #{user.created_at.to_date}

The default application layout will pick up the content_for :head code and add it to the head section of the application layout. The page will load the Javascript code for the Google Chart Tools API from Google’s content delivery network.

Generate a Chart with the User Controller

We’ll use the GoogleVisualr gem in the user controller to retrieve data and generate a chart.

Modify the controller file app/controllers/users_controller.rb:

class UsersController < ApplicationController
  before_filter :authenticate_user!

  def index
    authorize! :index, @user, :message => 'Not authorized as an administrator.'
    @users = User.all
    @chart = create_chart
  end
  
  def show
    @user = User.find(params[:id])
  end
  
  private
  
  def create_chart
    users_by_day = User.group("DATE(created_at)").count
    data_table = GoogleVisualr::DataTable.new
    data_table.new_column('date')
    data_table.new_column('number')
    users_by_day.each do |day|
      data_table.add_row([ Date.parse(day[0]), day[1]])
    end
    @chart = GoogleVisualr::Interactive::AnnotatedTimeLine.new(data_table)
  end

end

The UsersController will use the GoogleVisualr gem to interact with the Google Chart Tools API to generate a chart. Google Chart Tools offers a wide variety of charts (see examples) and the GoogleVisualr documentation shows how to implement many of the available charts. We use the Annotated Time Line chart.

Add a Chart to the Administrative Page

Let’s add the chart to the page that shows a list of users.

Modify the file app/views/users/index.html.haml:

- content_for :head do
  = javascript_include_tag 'http://www.google.com/jsapi'

#chart{:style => "width: 700px; height: 240px;"}
= render_chart @chart, 'chart'
  
%h2 Users
- @users.each do |user|
  %br/
  #{link_to user.email, user} signed up #{user.created_at.to_date}

The Annotated Time Line chart must be placed inside a div tag with a fixed width and height because it uses Adobe Flash Player to provide interactive features.

We now have a simple chart on the administrative page.

Git Workflow

Since the new feature is complete, merge the working branch to “master” and squash the commits so you have just one commit for the entire feature:

$ git checkout master
$ git merge --squash add-chart
$ git commit -am "add chart to admin dashboard"

You can delete the working branch when you’re done:

$ git branch -D add-chart

Improving the Design: Sorting a Table

We can make our administrative dashboard more useful by improving the display of the table of users. As implemented so far, it is just a simple list. We can add a jQuery plugin to give us a table that is sortable, searchable, and with pagination. The DataTables jQuery plugin is popular and integrates with Twitter Bootstrap. Ryan Bates offers a DataTables RailsCast that shows how to set it up using Robin Wenglewski’s jquery-datatables-rails gem.

First we’ll set up our git workflow so we can add a new feature.

Git Workflow

Create a new git branch for this feature:

$ git checkout -b sort-table

jQuery-Datatables-Rails Gem

We need to add the jquery-datatables-rails gem to the Gemfile:

gem "jquery-datatables-rails"

Run the bundle install command to install the required gem on your computer:

$ bundle install

Add the jQuery Plugin

Update the file app/assets/javascripts/application.js to include the jQuery plugin and its Bootstrap support library:

//= require jquery
//= require jquery_ujs
//= require bootstrap
//= require dataTables/jquery.dataTables
//= require dataTables/jquery.dataTables.bootstrap
//= require_tree .

Add CSS Rules

Update the file app/assets/stylesheets/application.css.scss to include CSS rules for DataTables that integrate with Twitter Bootstrap:

*= require_self
*= require dataTables/jquery.dataTables.bootstrap
*= require_tree .

Add JavaScript

We’ll need JavaScript for the administrative page where we will use DataTables. Add CoffeeScript to the file app/assets/javascripts/users.js.coffee:

#// For fixed width containers
jQuery ->
  $('.datatable').dataTable({
    "sDom": "<'row'<'span6'l><'span6'f>r>t<'row'<'span6'i><'span6'p>>",
    "sPaginationType": "bootstrap"
    });

Add a Table to the Administrative Page

Let’s add a table of users to the administrative page.

Modify the file app/views/users/index.html.haml:

- content_for :head do
  = javascript_include_tag 'http://www.google.com/jsapi'
%h2 Users
.span9
  #chart{:style => "width: 700px; height: 240px"}
  = render_chart @chart, 'chart'
  %br
.span9
  %table.datatable.table.table-bordered.table-condensed
    %thead
      %tr
        %th Email
        %th Signed up
    %tbody
      - @users.each do |user|
        %tr
          %td #{link_to user.email, user}
          %td #{user.created_at.to_date}

We now have a sortable, searchable table of users on the administrative page.

Note: The top line of the table (containing “Show entries” and “Search”) isn’t aligned nicely. Suggestions to improve it?

Scalability Issues

As implemented, our administrative page has a scalability constraint. In the Users controller, we query the database for all the users in the database and render a page containing a table of all the users. If we have many thousands of users requesting invitations, the administrative page will be slow to load. Ryan Bates shows how to implement pagination using server-side processing in his DataTables RailsCast. For a startup launch with a few thousand users, our implementation should be adequate.

Git Workflow

Since the new feature is complete, merge the working branch to “master” and squash the commits so you have just one commit for the entire feature:

$ git checkout master
$ git merge --squash sort-table
$ git commit -am "add sortable table on the admin dashboard"

You can delete the working branch when you’re done:

$ git branch -D sort-table

Feature: Send Invitations

As implemented, our application collects invitation requests from visitors. We’ve built a simple administrative dashboard displaying a graph of requests over time and a list of visitors’ email addresses. This is all you need while you are promoting and building your application. But when you ready for users to begin trying your site, you’ll want to select users for your beta test, send each an invitation, and ask each to confirm their email address and set an account password. We’ll call this functionality the “Send Invitations” feature and we’ll start implementing it with a user story and acceptance tests.

Git Workflow

Create a new git branch for this feature:

$ git checkout -b invitations

User Story

Here’s the user story we’ll specify and implement:

*Send Invitations*
As the owner of the site
I want to send invitations to visitors who have requested invitations
so users can try the site

Cucumber Scenario

Let’s write our specification.

Add the following file:

features/admin/send_invitations.feature

Feature: Send Invitations
  As the owner of the site
  I want to send invitations to visitors who have requested invitations
  so users can try the site

  Scenario: Administrator sends invitation
    Given I am logged in as an administrator
    When I visit the users page
    And I click a link "send invitation"
    And I open the email with subject "Confirmation instructions"
    Then I should see "confirm your account email" in the email body

This Cucumber feature file contains the specification needed to implement the user story “Send Invitations.”

Cucumber Step Definitions

Turn the specification into an automated acceptance test.

Add these steps to the existing file:

features/step_definitions/admin_steps.rb

When /^I click a link "([^"]*)"$/ do |arg1|
  click_on (arg1)
end

Be sure you’ve set up the database for testing before running Cucumber:

$ bundle exec rake db:test:prepare

Then we can run our integration test with the following command:

$ bundle exec cucumber features/admin/send_invitations.feature

The test will fail because we haven’t yet implemented the functionality.

Implementation

Devise already knows how to send an account confirmation email to a user, with its user.send_confirmation_instructions method, which will generate a confirmation token and send an email message to the user. We’ll add a “send invitation” link to each user listed on the administrative dashboard. We’ll need a corresponding invite action in the user controller and a matching route. We also want to make sure only an administrator is authorized to initiate the invite action.

Add a User Controller Action

So far, our application architecture has been very simple. We’ve used a few RESTful actions supplied by Devise and improved on a simple index method to prepare our administrative dashboard page. Now we’ll need a custom invite action in the user controller.

Modify the controller file app/controllers/users_controller.rb:

class UsersController < ApplicationController
  before_filter :authenticate_user!

  def index
    authorize! :index, @user, :message => 'Not authorized as an administrator.'
    @users = User.all
    @chart = create_chart
  end
  
  def show
    @user = User.find(params[:id])
  end
  
  def invite
    authorize! :invite, @user, :message => 'Not authorized as an administrator.'
    @user = User.find(params[:id])
    @user.send_confirmation_instructions
    redirect_to :back, :notice => "Sent invitation to #{@user.email}."
  end
  
  private
  
  def create_chart
    users_by_day = User.group("DATE(created_at)").count
    data_table = GoogleVisualr::DataTable.new
    data_table.new_column('date')
    data_table.new_column('number')
    users_by_day.each do |day|
      data_table.add_row([ Date.parse(day[0]), day[1]])
    end
    @chart = GoogleVisualr::Interactive::AnnotatedTimeLine.new(data_table)
  end

end

CanCan’s authorize! method ensures that only an administrator is authorized to initiate the invite action.

The invite action will use Devise’s supplied send_confirmation_instructions method to generate a confirmation token and send an email to the selected user. Then it will redirect the administrator back to a previous page.

Add a Route

Modify config/routes.rb to add the new action. Replace resources :users, :only => [:show, :index] with:

resources :users, :only => [:show, :index] do
  get 'invite', :on => :member
end

The Rails Guide, Routing from the Outside In, shows how we add additional routes to a RESTful resource.

Sending Invitations from the Administrative Page

We will modify the file app/views/users/index.html.haml to add a link to the invite action for each user. While we’re at it, we’ll add columns to show when the visitor joined (that is, confirmed their account), the number of times they’ve logged it, and the date of the most recent login.

- content_for :head do
  = javascript_include_tag 'http://www.google.com/jsapi'
%h2 Users
.span9
  #chart{:style => "width: 700px; height: 240px"}
  = render_chart @chart, 'chart'
  %br
.span9
  %table.datatable.table.table-bordered.table-condensed
    %thead
      %tr
        %th Email
        %th Requested
        %th Invitation
        %th Joined
        %th Visits
        %th Most Recent
    %tbody
      - @users.each do |user|
        %tr
          %td #{link_to user.email, user}
          %td #{user.created_at.to_date}
          %td #{(user.confirmation_token.nil? ? (link_to "send invitation", invite_user_path(user)) : (link_to "resend", invite_user_path(user))) unless user.confirmed_at}
          %td #{user.confirmed_at.to_date if user.confirmed_at}
          %td #{user.sign_in_count if user.sign_in_count}
          %td #{user.last_sign_in_at.to_date if user.last_sign_in_at}

Now, as the site administrator, you can invite individual users to complete the account confirmation process and obtain access to the site.

There’s a bit of complex logic in the display of the “send invitation” link. First, with unless user.confirmed_at, we check to see if the user has already confirmed the account. Next, we check the user’s confirmation_token attribute to see if we’ve already sent an invitation (in which case, a confirmation token was set by Devise). If no confirmation token was set, we display the link “send invitation”; otherwise, we display “resend”.

Bulk Invitations

If you are selecting only a few dozen (or a few hundred) initial users, this process of manual selection will be adequate. If you are ready to launch and want to invite many thousand users, you’ll need a way to invite multiple users with a single action. We need to implement a “bulk invitations” feature.

We’ll add a bulk_invite action to the controller file app/controllers/users_controller.rb:

class UsersController < ApplicationController
  before_filter :authenticate_user!

  def index
    authorize! :index, @user, :message => 'Not authorized as an administrator.'
    @users = User.all
    @chart = create_chart
  end
  
  def show
    @user = User.find(params[:id])
  end
  
  def invite
    authorize! :invite, @user, :message => 'Not authorized as an administrator.'
    @user = User.find(params[:id])
    @user.send_confirmation_instructions
    redirect_to :back, :notice => "Sent invitation to #{@user.email}."
  end
  
  def bulk_invite
    authorize! :bulk_invite, @user, :message => 'Not authorized as an administrator.'
    users = User.where(:confirmation_token => nil).order(:created_at).limit(params[:quantity])
    users.each do |user|
      user.send_confirmation_instructions
    end
    redirect_to :back, :notice => "Sent invitation to #{users.count} users."
  end
  
  private
  
  def create_chart
    users_by_day = User.group("DATE(created_at)").count
    data_table = GoogleVisualr::DataTable.new
    data_table.new_column('date')
    data_table.new_column('number')
    users_by_day.each do |day|
      data_table.add_row([ Date.parse(day[0]), day[1]])
    end
    @chart = GoogleVisualr::Interactive::AnnotatedTimeLine.new(data_table)
  end

end

The bulk_invite action has a few peculiarities. First, we only select users where the :confirmation_token attribute is null. Devise sets a confirmation token when we send an invitation; with this constraint, we only invite users who have not been previously invited. Second, we make sure we select the oldest records first with the order(:created_at) method. Finally, we limit the number of records that are retrieved to the size of the batch we want; then we loop over each to send an invitation.

Modify config/routes.rb to add the new action:

RailsPrelaunchSignup::Application.routes.draw do
  authenticated :user do
    root :to => 'home#index'
  end
  devise_scope :user do
    root :to => "devise/registrations#new"
    match '/user/confirmation' => 'confirmations#update', :via => :put, :as => :update_user_confirmation
  end
  devise_for :users, :controllers => { :registrations => "registrations", :confirmations => "confirmations" }
  match 'users/bulk_invite/:quantity' => 'users#bulk_invite', :via => :get, :as => :bulk_invite
  resources :users, :only => [:show, :index] do
    get 'invite', :on => :member
  end
end

Finally, we will modify the file app/views/users/index.html.haml to add links for the bulk_invite action:

- content_for :head do
  = javascript_include_tag 'http://www.google.com/jsapi'
%h2 Users
.span9
  #chart{:style => "width: 700px; height: 240px"}
  = render_chart @chart, 'chart'
  %br
.span9
  %p 
    Send Bulk Invitations: 
    = link_to "10 &#183;".html_safe, bulk_invite_path(:quantity => '10')
    = link_to "50 &#183;".html_safe, bulk_invite_path(:quantity => '50')
    = link_to "100 &#183;".html_safe, bulk_invite_path(:quantity => '100')
    = link_to "500 &#183;".html_safe, bulk_invite_path(:quantity => '500')
    = link_to "1000", bulk_invite_path(:quantity => '1000')
  %table.datatable.table.table-bordered.table-condensed
    %thead
      %tr
        %th Email
        %th Requested
        %th Invitation
        %th Joined
        %th Visits
        %th Most Recent
    %tbody
      - @users.each do |user|
        %tr
          %td #{link_to user.email, user}
          %td #{user.created_at.to_date}
          %td #{(user.confirmation_token.nil? ? (link_to "send invitation", invite_user_path(user)) : (link_to "resend", invite_user_path(user))) unless user.confirmed_at}
          %td #{user.confirmed_at.to_date if user.confirmed_at}
          %td #{user.sign_in_count if user.sign_in_count}
          %td #{user.last_sign_in_at.to_date if user.last_sign_in_at}

We now have the option to send 10, 50, 100, 500, or 1000 invitations at once.

Git Workflow

Since the new feature is complete, merge the working branch to “master” and squash the commits so you have just one commit for the entire feature:

$ git checkout master
$ git merge --squash invitations
$ git commit -am "enable admin to send invitations"

You can delete the working branch when you’re done:

$ git branch -D invitations

Setting the User’s Password

Once the user has been invited and has confirmed the account, we need to provide a way for the user to set a password.

The Devise wiki explains How to Override Confirmations So Users Can Pick Their Own Passwords As Part of Confirmation Activation. So far, we’ve made small modifications to customize the behavior of Devise; now we’ll make extensive modifications, overriding the Confirmations controller and view as well as adding methods to the User model.

Git Workflow

Create a new git branch for this improvement:

$ git checkout -b fix-password

Customize the Confirmations View

When an invited user clicks a link in the invitation email to confirm an account, we want to ask them to choose a password for their account. The Devise gem supplies a Confirmations view; we need to replace it with a page that includes fields for password and password confirmation.

Create a new folder app/views/devise/confirmations/.

Create a file app/views/devise/confirmations/show.html.haml:

%h2 Account Activation
= form_for resource, :as => resource_name, :url => update_user_confirmation_path, :html => {:method => 'put'}, :id => 'activation-form' do |f|
  = devise_error_messages!
  %fieldset
    %legend
      Account Activation
      - if resource.email
        for #{resource.email}
    - if @requires_password
      %p
        = f.label :password,'Choose a Password:'
        = f.password_field :password
      %p
        = f.label :password_confirmation, 'Password Confirmation:'
        = f.password_field :password_confirmation
    = hidden_field_tag :confirmation_token, @confirmation_token
    %p= f.submit "Activate"

Customize the Confirmations Controller

The Devise gem supplies a Confirmations controller; we need to replace it with one that will accommodate our customized view that asks the user to set a password. We’ll override the Devise Confirmations controller by creating our own Confirmations controller. Often, in overriding a controller, we inherit from the original controller. In this case, we’ll inherit from the Devise Passwords controller that provides methods to set the user’s password.

Create a file app/controllers/confirmations_controller.rb:

class ConfirmationsController < Devise::PasswordsController
  # Remove the first skip_before_filter (:require_no_authentication) if you
  # don't want to enable logged users to access the confirmation page.
  skip_before_filter :require_no_authentication
  skip_before_filter :authenticate_user!

  # PUT /resource/confirmation
  def update
    with_unconfirmed_confirmable do
      if @confirmable.has_no_password?
        @confirmable.attempt_set_password(params[:user])
        if @confirmable.valid?
          do_confirm
        else
          do_show
          @confirmable.errors.clear #so that we won't render :new
        end
      else
        self.class.add_error_on(self, :email, :password_allready_set)
      end
    end

    if [email protected]?
      render 'devise/confirmations/new'
    end
  end

  # GET /resource/confirmation?confirmation_token=abcdef
  def show
    with_unconfirmed_confirmable do
      if @confirmable.has_no_password?
        do_show
      else
        do_confirm
      end
    end
    if [email protected]?
      render 'devise/confirmations/new'
    end
  end
  
  protected

  def with_unconfirmed_confirmable
    @confirmable = User.find_or_initialize_with_error_by(:confirmation_token, params[:confirmation_token])
    if [email protected]_record?
      @confirmable.only_if_unconfirmed {yield}
    end
  end

  def do_show
    @confirmation_token = params[:confirmation_token]
    @requires_password = true
    self.resource = @confirmable
    render 'devise/confirmations/show'
  end

  def do_confirm
    @confirmable.confirm!
    set_flash_message :notice, :confirmed
    sign_in_and_redirect(resource_name, @confirmable)
  end
end

Add a Route

Modify config/routes.rb to use the new controller:

RailsPrelaunchSignup::Application.routes.draw do
  authenticated :user do
    root :to => 'home#index'
  end
  devise_scope :user do
    root :to => "devise/registrations#new"
    match '/user/confirmation' => 'confirmations#update', :via => :put, :as => :update_user_confirmation
  end
  devise_for :users, :controllers => { :registrations => "registrations", :confirmations => "confirmations" }
  resources :users, :only => [:show, :index] do
    get 'invite', :on => :member
  end
end

Modify the User Model

We need to modify the User model to allow the new user to set a password when they confirm their account.

Add the following methods to the file app/models/user.rb

# new function to set the password
def attempt_set_password(params)
  p = {}
  p[:password] = params[:password]
  p[:password_confirmation] = params[:password_confirmation]
  update_attributes(p)
end

# new function to determine whether a password has been set
def has_no_password?
  self.encrypted_password.blank?
end

# new function to provide access to protected method pending_any_confirmation
def only_if_unconfirmed
  pending_any_confirmation {yield}
end

Git Workflow

Merge the new code into the master branch and commit it:

$ git checkout master
$ git merge --squash fix-password
$ git commit -am "enable a user to set a password"

You can delete the working branch when you’re done:

$ git branch -D fix-password

Test the App

You can check that your app runs properly by entering the command

$ rails server

To see your application in action, open a browser window and navigate to http://localhost:3000/.

Sign in as the first user (the administrator) using:

You’ll see a navigation link for Admin. Clicking the link will display a page with a list of users at
http://localhost:3000/users.

To sign in as the second user, use

The second user will not see the Admin navigation link and will not be able to access the page at
http://localhost:3000/users.

If you want to see what the admininstrative dashboard looks like with many users, you can add a line to the db/seeds.rb file to create a hundred bogus users:

100.times {|i| User.create! :name => "User #{i+3}", :email => "user#{i+3}@example.com", :password => 'please', :password_confirmation => 'please', :confirmed_at => (Time.now + i.day).utc, :created_at => (Time.now + i.day).utc  }

You’ll have to modify the file app/models/user.rb to allow mass assignment of the created_at field:

attr_accessible :name, :email, :password, :password_confirmation, :remember_me, :confirmed_at, :created_at

Then run $ bundle exec rake db:reset to recreate the database and visit the site again.

More to Come

The tutorial is not yet finished. There will be more to come. Specifically a feature to allow visitors to post to Facebook, Twitter, or Google+ after they request an invitation.

Deploy to Heroku

Heroku provides low cost, easily configured Rails application hosting. For your convenience, see Tutorial: Rails on Heroku.

Conclusion

This concludes the tutorial for the RailsApp rails-prelaunch-signup example app.

Did You Like the Tutorial? Here’s What You Can Do

Was this useful to you? Follow rails_apps on Twitter and tweet some praise. I’d love to know you were helped out by the tutorial.

Get some link juice! Add your website to the list of Rails Applications Built from the Examples. I love to see what people have built with these examples.

Blog it! Share your discovery with the startup community. It’s up to you to get the word out and help fellow entrepreneurs. Links are the best way for people to find resources like this.

Please leave a comment to tell me how long it took for you to read the tutorial and complete the app.

Any issues? Please create an issue on GitHub. Reporting (and patching!) issues helps everyone.

Credits

Daniel Kehoe implemented the application and wrote the tutorial.

Clone this wiki locally