my face
About Me

Published Posts

All Posts

New Post


View by Tag:

interviewing, code, testing, philosophy, blog, wantmyjob, virtualization, railsmud, heroku, ruby, published, neoarchaeology, railsgame, rails, juggernaut, astrino, cheaptoad, shannaspizza, mongodb, refactorit, devise, rvm, passenger


FeedBurner picture


Online Portfolio

Resume

Profile on LinkedIn

Recommend Me

Building Refactor It, a Rails3 App, in Steps

In this series, I build a simple Rails 3 app. You can follow along, or just read what you need. There's a GitHub repository for this series, with tags for the different parts.

Part One: From Nothing to Logging In

If you haven't already, run sudo gem install rails --pre to get Rails 3 installed (note: after it's released, you can skip the "--pre"). I also assume you have a copy of git (on Debian or Ubuntu: sudo apt-get install git-core).

In this series, I'm going to create a very simple social application that lets you post a code snippet to your web app and anybody can suggest different refactorings of that snippet. That's the kind of app Rails is really good for.

First I create a new, empty Rails 3 app:

   1  angelbob@dell-desktop:~/src/rails$ rails new refactor
   2        create  
   3        create  README
   4        create  Rakefile
   5        create  config.ru
   6        create  .gitignore
   7        (Cut for length...)
   8        create  vendor/plugins/.gitkeep

Rails just created a lot of stuff. That's a good thing, it turns out. For instance, that means it's already set up for git. Let's do that:

   1  angelbob@dell-desktop:~/src/rails$ cd refactor
   2  angelbob@dell-desktop:~/src/rails/refactor$ ls
   3  app     config.ru  doc      lib  public    README  test  vendor
   4  config  db         Gemfile  log  Rakefile  script  tmp
   5  angelbob@dell-desktop:~/src/rails/refactor$ git init
   6  Initialized empty Git repository in /home/angelbob/src/rails/refactor/.git/
   7  angelbob@dell-desktop:~/src/rails/refactor$ git add .
   8  angelbob@dell-desktop:~/src/rails/refactor$ git commit -m "New rails3 project"
   9  [master (root-commit) f17dd29] New rails3 project
  10   39 files changed, 9083 insertions(+), 0 deletions(-)
  11   create mode 100644 .gitignore
  12   create mode 100644 Gemfile
  13   create mode 100644 README
  14   create mode 100644 Rakefile
  15   (Cut for length...)
  16   create mode 100644 vendor/plugins/.gitkeep
  17  angelbob@dell-desktop:~/src/rails/refactor$

We don't have to connect git to anything outside your computer. We can just keep this local repository for tracking. I also set up a GitHub repository for this series, but that's just so that you can see my progress and verify any file that I don't explain well :-)

We'll want users of our app to log in when submitting code. I think devise is exactly the solution to use for login here. So let's set it up.

   1  angelbob@dell-desktop:~/src/rails/refactor$ sudo gem install devise --pre
   2  Building native extensions.  This could take a while...
   3  Successfully installed warden-0.10.7
   4  Successfully installed bcrypt-ruby-2.1.2
   5  Successfully installed devise-1.1.rc2
   6  3 gems installed
   7  Installing ri documentation for warden-0.10.7...
   8  Installing ri documentation for bcrypt-ruby-2.1.2...
   9  Installing ri documentation for devise-1.1.rc2...
  10  Installing RDoc documentation for warden-0.10.7...
  11  Installing RDoc documentation for bcrypt-ruby-2.1.2...
  12  Installing RDoc documentation for devise-1.1.rc2...

Awesome! Next, let's do some devise configuration.

   1  angelbob@dell-desktop:~/src/rails/refactor$ rails generate devise:install
   2  Could not find generator devise:install.

Huh? Oh, wait, we can't see devise yet. This is Rails 3, so any gem we want to use needs to be declared to the Bundler. So I'll edit the Gemfile:

   1  source 'http://rubygems.org'
   2  
   3  gem 'rails', '3.0.0.beta4'
   4  
   5  # Bundle edge Rails instead:
   6  # gem 'rails', :git => 'git://github.com/rails/rails.git'
   7  
   8  gem 'sqlite3-ruby', :require => 'sqlite3'
   9  
  10  # Use unicorn as the web server
  11  # gem 'unicorn'
  12  
  13  # Deploy with Capistrano
  14  # gem 'capistrano'
  15  
  16  # To use debugger
  17  # gem 'ruby-debug'
  18  
  19  # Bundle the extra gems:
  20  # gem 'bj'
  21  # gem 'nokogiri', '1.4.1'
  22  # gem 'sqlite3-ruby', :require => 'sqlite3'
  23  # gem 'aws-s3', :require => 'aws/s3'
  24  
  25  gem 'devise', '1.1.rc2'
  26  
  27  # Bundle gems for certain environments:
  28  # gem 'rspec', :group => :test
  29  # group :test do
  30  #   gem 'webrat'
  31  # end

If you look closely, you'll see that I added a line for devise in there. I'm using the prerelease 1.1.rc2, though you can use whatever version you just installed. Now, let's try again:

   1  angelbob@dell-desktop:~/src/rails/refactor$ rails generate devise:install
   2  [WARNING] config.encryptor is not set in your config/initializers/devise.rb. Devise will then set it to :bcrypt. If you were using the previous default encryptor, please add config.encryptor = :sha1 to your configuration file.
   3        create  config/initializers/devise.rb
   4        create  config/locales/devise.en.yml
   5  
   6  ===============================================================================
   7  
   8  Some setup you must do manually if you haven't yet:
   9  
  10    1. Setup default url options for your specific environment. Here is an
  11       example of development environment:
  12  
  13         config.action_mailer.default_url_options = { :host => 'localhost:3000' }
  14  
  15       This is a required Rails configuration. In production it must be the
  16       actual host of your application
  17  
  18    2. Ensure you have defined root_url to *something* in your config/routes.rb.
  19       For example:
  20  
  21         root :to => "home#index"
  22  
  23    3. Ensure you have flash messages in app/views/layouts/application.html.erb.
  24       For example:
  25  
  26         <p class="notice"><%= notice %></p>
  27         <p class="alert"><%= alert %></p>
  28  
  29  ===============================================================================

Devise has just given us some good advice. We should take it. Let's create the new root controller first:

   1  angelbob@dell-desktop:~/src/rails/refactor$ rails generate controller Welcome index
   2        create  app/controllers/welcome_controller.rb
   3         route  get "welcome/index"
   4        invoke  erb
   5        create    app/views/welcome
   6        create    app/views/welcome/index.html.erb
   7        invoke  test_unit
   8        create    test/functional/welcome_controller_test.rb
   9        invoke  helper
  10        create    app/helpers/welcome_helper.rb
  11        invoke    test_unit
  12        create      test/unit/helpers/welcome_helper_test.rb
  13  angelbob@dell-desktop:~/src/rails/refactor$ git rm public/index.html
  14  rm 'public/index.html'

For now, we'll ignore the test stuff rather than installing a real test framework and using it. The BDD people will tell you that this makes me a bad person. Fair enough. You're welcome to do it however you like at home, but I'm going to show you how to set up devise next ;-)

Next, take devise's advice and added the ActionMailer configuration option to config/environments/development.html. I also added a div with the id "flash_container" to my app/views/layouts/application.html file with the two lines devise told me to. You should do the same. If you have any trouble, check the GitHub repository for what those files look like. You may want to look at the tag "one", for the ways all files look in this part, not how they look after every part I've written.

The next thing for devise is to create a model for its user/login objects. Devise supports all kinds of crazy stuff with all kinds of multiple permissions, but we only really need one flavor of user for this:

   1  angelbob@dell-desktop:~/src/rails/refactor$ rails generate devise User
   2        invoke  active_record
   3        create    app/models/user.rb
   4        invoke    test_unit
   5        create      test/unit/user_test.rb
   6        create      test/fixtures/users.yml
   7        inject  app/models/user.rb
   8        create  db/migrate/20100626034353_devise_create_users.rb
   9         route  devise_for :users

Devise uses Rails engines and other stuff to add a bunch of fun UI to your app. That's why it added a route, for instance. So now we have the ability to create users, log in and out and so on. At least, we will once we run the migration:

   1  class DeviseCreateUsers < ActiveRecord::Migration
   2    def self.up
   3      create_table(:usersdo |t|
   4        t.database_authenticatable :null => false
   5        t.recoverable
   6        t.rememberable
   7        t.trackable
   8  
   9        # t.confirmable
  10        # t.lockable :lock_strategy => :failed_attempts, :unlock_strategy => :both
  11        # t.token_authenticatable
  12  
  13        t.timestamps
  14      end
  15  
  16      add_index :users, :email,                :unique => true
  17      add_index :users, :reset_password_token, :unique => true
  18      # add_index :users, :confirmation_token,   :unique => true
  19      # add_index :users, :unlock_token,         :unique => true
  20    end
  21  
  22    def self.down
  23      drop_table :users
  24    end
  25  end

Each of those lines is a thing you can potentially do with devise users. "Confirmable", for instance, means that you can have devise send email to confirm the user, and require clicking on a link to verify. "Lockable" is to lock somebody out for giving the wrong password too many times. Token_authenticatable is to let you log in with a token rather than using the regular database-and-input-form web-based method.

If you uncomment everything, you can just configure it in the model, but your users table is slightly larger than needed. I'll do that in case I want to use any of these things later. Also, I'll include both indices. Users get looked up a lot and they're added rarely, so you can index them to Chattanooga and back without it being a big problem. And I want a "username" field, so I'm putting in "t.string :username, :limit => 50", because I'm like that. I don't like logging in with an email address.

And then, the model:

   1  class User < ActiveRecord::Base
   2  
   3    # Include default devise modules. Others available are:
   4    # :token_authenticatable, :confirmable, :lockable and :timeoutable
   5    devise :database_authenticatable, :registerable,
   6           :recoverable, :rememberable, :trackable, :validatable
   7  
   8    # Setup accessible (or protected) attributes for your model
   9    attr_accessible :email, :password, :password_confirmation
  10  end

So what's all that? Well, registerable lets you create the users, confirmable sends email and has you click on a link, recoverable gives password recovery, rememberable gives a "remember me" link, lockable lets you lock them out configurably, and timeoutable logs them out after a while. All this stuff is remarkably poorly documented, and it's kinda scattered in the source code for devise. You can find something about some of them in devise's initializer (config/initializers/devise.rb). Luckily, they all have pretty intuitive names, and the code to devise is pretty readable.

I'm just uncommenting everything except :token_authenticatable and :confirmable, because I like login timeouts and login lockouts. You can pick a different set of stuff if you like. Do NOT uncomment :confirmable unless you're comfortable picking through the Rails logfile. Remember that ActionMailer doesn't work right until you configure it, and "confirmable" means you have to get the email and click a link before you can use your account.

The initializer (config/initializers/devise.rb) will let you change the "from" email address for confirmation emails (if you have them), the timeout for logins, the number of unsuccessful login attempts and many other things. I like :username to be the authentication key, for instance. But I won't change that yet, because that requires customizing the views. We'll get there.

You'll want to run the rails server to test stuff with. In Rails 3, that means running "rails server" from the rails app directory. Give it a try. Then, open a web browser and point it at "http://localhost:3000". You'll probably see something like this message:

   1  Welcome#index
   2  
   3  Find me in app/views/welcome/index.html.erb

Perfect! We added the controller and didn't customize it, so that's what we should be seeing. Let's change the view a bit:

   1  <% if current_user %>
   2    Logged in as <%= current_user.email %>:
   3    <%= link_to 'Sign Out', destroy_user_session_url %>
   4  <% else %>
   5    <%= link_to 'Sign In', new_user_session_url %> or
   6    <%= link_to 'Register', new_user_registration_url %>
   7  <% end %>

Now reload, and hit "sign in" or "register". You should see a page like the one below to let you log in or create an account. It's being served through the devise gem so you can customize it later if you want.

screenshot

Creating an account is good. Do that. Notice that you can't enter a username yet, but supply an email address (don't worry -- you won't sell your address to anybody ;-)

That's not a bad amount of work to get a full user signup system for your app!

If You Ignored My Advice...

Let's say you ignored what I said up above and turned on "confirmable" in the User model. It's unlikely that Rails can get email sent, so you should go to the console where you're running the server. You'll see something like this in the log:

   1  Started POST "/users" for 127.0.0.1 at Fri Jun 25 21:55:04 -0700 2010
   2    Processing by Devise::RegistrationsController#create as HTML
   3    Parameters: {"commit"=>"Sign up", "authenticity_token"=>"vhmK0PymfGUPcXbS/d/7/8UwyNoO5Qt7Q+CfVdIlJVc=", "user"=>{"password_confirmation"=>"[FILTERED]", "password"=>"[FILTERED]", "email"=>"somebody@somedomain.com"}}
   4    SQL (0.3ms)   SELECT name
   5   FROM sqlite_master
   6   WHERE type = 'table' AND NOT name = 'sqlite_sequence'
   7  
   8    User Load (0.2ms)  SELECT "users"."id" FROM "users" WHERE ("users"."email" = 'somebody@somedomain.com') LIMIT 1
   9    SQL (0.6ms)  INSERT INTO "users" ("authentication_token", "confirmation_sent_at", "confirmation_token", "confirmed_at", "created_at", "current_sign_in_at", "current_sign_in_ip", "email", "encrypted_password", "failed_attempts", "last_sign_in_at", "last_sign_in_ip", "locked_at", "password_salt", "remember_created_at", "remember_token", "reset_password_token", "sign_in_count", "unlock_token", "updated_at", "username") VALUES (NULL, '2010-06-26 04:55:04.734561', 'S7oc9zvXoY1_rnxdBQfH', NULL, '2010-06-26 04:55:04.734629', NULL, NULL, 'noah_gibbs@yahoo.com', '$2a$10$GDmudCAKDYFPMMoHGiB4seTPlvuXExgMJfgAHDLhaaNzfQEcTVOE2', 0, NULL, NULL, NULL, '$2a$10$GDmudCAKDYFPMMoHGiB4se', NULL, NULL, NULL, 0, NULL, '2010-06-26 04:55:04.734629', NULL)
  10  Rendered /usr/local/lib/ruby/gems/1.8/gems/devise-1.1.rc2/app/views/devise/mailer/confirmation_instructions.html.erb (1.2ms)
  11  
  12  Sent mail to noah_gibbs@yahoo.com (77ms)
  13  Date: Fri, 25 Jun 2010 21:55:04 -0700
  14  From: Refactorer@refactor.angelbob.com
  15  To: noah_gibbs@yahoo.com
  16  Message-ID: <4c258828d57ee_55c924903fd27737@dell-desktop.mail>
  17  Subject: Confirmation instructions
  18  Mime-Version: 1.0
  19  Content-Type: text/html;
  20   charset=UTF-8
  21  Content-Transfer-Encoding: 7bit
  22  
  23  <p>Welcome noah_gibbs@yahoo.com!</p>
  24  
  25  <p>You can confirm your account through the link below:</p>
  26  
  27  <p><a href="http://localhost:3000/users/confirmation?confirmation_token=S7oc9zvXoY1_rnxdBqfH">Confirm my account</a></p>
  28  Completed   in 376ms

If you turned on confirmations, you'll note that there's a confirmation URL you'll need to go to. Copy it out of the log, and go there in your browser, which will activate your account.

blog comments powered by Disqus