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
- Part Two: Nested Resources
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.
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
