gem install sqlite3-ruby)Execute the steps prefixed with $.
$ rail quotes
...
$ cd quotes
$ script/generate model
observe the help message.
$ script/generate model quote body:text source:string
...
create app/models/quote.rb
create test/unit/quote_test.rb
create test/fixtures/quotes.yml
create db/migrate
create db/migrate/XXX_create_quotes.rb
$
Open the generated files quote.rb with your favourite text editor; observe it’s almost empty. Open db/migrate/XXX_create_quotes.rb (where XXX varies). Read it. Read also app/models/quote.rb.
$ rake db:migrate
Observe that the database schema was created:
$ sqlite3 db/development.sqlite3
SQLite version 3.6.4
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> .help
Read about the various commands available. Now ask for the list of tables:
sqlite> .tables
quotes schema_migrations
Now observe the sql schema generation code that was used to create them:
sqlite> .schema quotes
CREATE TABLE "quotes" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "author" varchar(255), "body" text, "created_at" datetime, "updated_at" datetime);
sqlite> .schema schema_migrations
CREATE TABLE "schema_migrations" ("version" varchar(255) NOT NULL);
CREATE UNIQUE INDEX "unique_schema_migrations" ON "schema_migrations" ("version");
Now observe the contents of those tables:
sqlite> select * from quotes;
(no contents yet)
sqlite> select * from schema_migrations;
20081226092052
(only one row is present)
sqlite> .quit
$
Now we know how to interact with Sqlite3, but we are not going to use that very much in the future, for we will interact with the database using ActiveRecord, as much as possible. There is a command-line interface to our Rails code, which is used in preference to the raw database interface for most tasks.
Let’s use this interface to add a few rows to the Quote model. (You may use your own favourite quotes, there’s no need to memorize these.)
$ script/console
Loading development environment (Rails 2.2.2)
>> Quote.find :all
=> []
No rows yet.
>> Quote.create(:author => 'Edsger Dijkstra', :body => "Computer science is no more about computers than astronomy is about telescopes.")
=> #<Quote id: 1, author: "Edsger Dijkstra", body: "Computer science is no more about computers than as...", created_at: "2008-12-26 09:34:46", updated_at: "2008-12-26 09:34:46">
Method Quote.create creates a new Quote object and saves it in the database.
>> Quote.count
=> 1
This shows there is a row now. Now let’s create another:
>> Quote.create(:author => 'Geoffrey James', :body => ' Thus spake the master programmer: "A well-written program is its own heaven; a poorly-written program is its own hell."')
=> #<Quote id: 2, author: "Geoffrey James", body: " Thus spake the master programmer: \"A well-written ...", created_at: "2008-12-26 09:36:46", updated_at: "2008-12-26 09:36:46">
>> Quote.count
=> 2
>> Quote.find :all
=> [#<Quote id: 1, author: "Edsger Dijkstra", body: "Computer science is no more about computers than as...", created_at: "2008-12-26 09:34:46", updated_at: "2008-12-26 09:34:46">, #<Quote id: 2, author: "Geoffrey James", body: " Thus spake the master programmer: \"A well-written ...", created_at: "2008-12-26 09:36:46", updated_at: "2008-12-26 09:36:46">]
>> quit
$
Thus we added two rows to the quotes table. The next step is about showing quotes in a web page.
Create a new controller.
$ script/generate controller quotes
Edit app/controllers/quotes_controller.rb; change it so that it contains this:
class QuotesController < ApplicationController
def index
@quotes = Quote.find :all
end
end
Create a new file app/views/quotes/index.html.erb. Insert this:
<h1>All quotes</h1>
<% for quote in @quotes %>
<div class="quote">
<p>
<%= quote.body %>
</p>
<p>
— <em><%= quote.author %></em>
</p>
</div>
<% end %>
Start the application.
$ script/server
Open the browser at http://localhost:3000/. You’ll see the standard Rails welcome page. Enter the url http://localhost:3000/quotes, and observe the list of quotes.
Now we want to make this page the home page of our application. Open config/routes.rb, and observe it says
# You can have the root of your site routed with map.root -- just remember to delete public/index.html.
# map.root :controller => "welcome"
So we do that: uncomment the second line and change “welcome” to “quotes”. Delete public/index.html. Reload http://localhost:3000/ in the browser and observe that the root page is now the list of all quotes.
The next step is to add an action to enter a new quote from the web.
new and createEdit app/views/quotes/index.html.erb and add this link to the bottom:
<%= link_to "Add new quote", :action => "new" %>
Reload the page. Observe the generated html. Click on the link and observe the error message. We need to add a new action. First create the view: app/views/quotes/new.html.erb
<h1>New quote</h1>
<form method="post" action="/quotes/create">
<p>
Body<br/>
<textarea name="quote_body" cols="40" rows="6"></textarea>
</p>
<p>
Author <br/>
<input type="text" name="quote_author" />
</p>
<p>
<input type="submit" value="Create">
</p>
</form>
Reload the /quotes/new page and observe our new, shining form. Cute, isnt’t it? Insert some random text in the fields and click the “Create” button. Observe the error message. We expected something like “Action create does not exist”, but we get something like ...InvalidAuthenticityToken. What’s happening here?
It seems that Rails is trying to protect us from forgeries, such as invoking the create action directly, without using the new form. It expects that the new form inserts some secret code in the request, that Rails checks whenever it receives an HTTP “POST” message. This is all very good, but we are not interested in that right now. We disable this security feature by opening app/controllers/application.rb and commenting out the call to protect_from_forgery. Now click again on the “Create” button and observe the expected message “No action responded to create”.
Add the create method to the quotes controller.
def create
quote = Quote.new(:author => params[:quote_author],
:body => params[:quote_body])
quote.save
redirect_to :action => "index"
end
The call to redirect_to will ensure that the page the user will see is the result of a GET http message, not a POST. Reload the page on the browser. The browser asks if we want to send again the contents of a “POST” message; we do, so we answer “yes”. We should see the home page with the new quote inserted.
A good rule is to never save bad data in the database. The usual solution to this is to validate code before saving it; if the user enters bad data, we will not save it, and we prompt the user to correct it.
Our validation will be to ensure that it is impossible to save a quote with empty body or author. Point the browser to /quotes/new and click “Create”, with no text in the text fields. Observe that an empty quote was created. This was bad data.
Now change app/models/quote.rb so that it contains:
class Quote < ActiveRecord::Base
validates_presence_of :body, :author
end
Change the create method in the controller so that it becomes
def create
quote = Quote.new(:author => params[:quote_author],
:body => params[:quote_body])
if quote.save
redirect_to :action => "index"
else
render :action => "new"
end
end
If the validation fails, quote.save returns false. In that case we don’t redirect, but we render again the template for the new action. (Try it now.) We should show an appropriate error message. Add in new.html.erb the following inside the form element:
<%= error_messages_for :quote %>
For this to work, we need to pass the invalid object to the view in a @quote instance variable. Change the create method to
def create
@quote = Quote.new(:author => params[:quote_author],
:body => params[:quote_body])
if @quote.save
# ...
end
end
Try again to insert an empty quote. You should see the message “2 errors prohibited this quote from being saved; There were problems with the following fields: … “. Try filling in the author, and leaving the body blank. Now the error messages changes to “1 error prohibited …”. We notice that the author that we typed before is not appearing again in the form. This is because every time we present the form to the user, the text fields are initialized to empty strings. To correct this we change the form to
<form method="post" action="/quotes/create">
<%= error_messages_for :quote %>
<p>
Body<br/>
<textarea name="quote_body" cols="40" rows="6"><%= @quote.body %></textarea>
</p>
<p>
Author <br/>
<input type="text" name="quote_author" value="<%= @quote.author %>"/>
</p>
<p>
<input type="submit" value="Create">
</p>
</form>
Now we see the form with the incomplete data. There is a problem though: the view now expects to always have a @quote variable. It works in the case of a failed validation (in the create action) but it will fail in the new action. Try it and observe the error message. This is easily corrected by defining a new method in the controller like this:
def new
@quote = Quote.new
end
Try again and see that it work correctly.
The html code we wrote so far works, but is not ideal. For one thing, the /quotes/update url is wired in; if we were to change the shape of the urls in routes.rb, it would break. We had to spell out the list of parameters in the create method; two are not many, but in real examples there could be dozens. And there is a lot of repetitions; see how many times the body and author words appear in it. This is not the rails way.
We rewrite the form with the Rails helpers.
<% form_for(@quote, :url => {:action => "create" }) do |f| %>
<%= f.error_messages %>
<p>
Body <br/>
<%= f.text_area :body, :size => "40x6" %>
</p>
<p>
Author <br/>
<%= f.text_field :author %>
</p>
<p>
<%= f.submit "Create" %>
</p>
<% end %>
Try it again. You should that it’s now impossible to save a new quote. The reason is that we didn’t update the action on the controller. The old create actions expects parameters :quote_author, :quote_body; the form helpers pass different parameters. We observe them by inserting raise "boom!" in the update action. The error page shows:
{
"commit"=>"Create",
"quote"=> {"body"=>"foo", "author"=>"bar"}
}
So the parameters for the quote are grouped in a hash pointed by “quote”. We change the create action like this:
def create
@quote = Quote.new(params[:quote])
if @quote.save
...
end
Now the parameters for new are much more concise. What’s more important, adding or changing attributes to the Quote model does not require changes to this action!
edit and update actionsWe now want to add a way to edit a quote. Open the index.html.erb view add the edit link:
<div class="quote">
...
<p>
<%= link_to "Edit", :action => "edit", :id => quote.id %>
</p>
</div>
Add the edit method to the controller:
def edit
@quote = Quote.find(params[:id])
end
(Now we see what is the use of the :id param that we pass in the link.) Create file app/views/quotes/edit.html.erb, copy the contents of new.html.erb in it and change it like this:
<h1>Edit quote</h1>
<% form_for(@quote, :url => {:action => "update", :id => @quote }) do |f| %>
(... same as in the new form ...)
<p>
<%= f.submit "Update" %>
</p>
<% end %>
We changed: the heading, the action we will call, and the text on the submit button. Everything else stays the same. Now click on an “edit” button and see that the form looks ok. It should be loaded with the text from the quote.
Repetition is bad, so we take the lines that are common within the two forms and save them in a partial template. Create file app/views/quotes/_form.html.erb (note the underscore) and copy the following lines in it:
<%= f.error_messages %>
<p>
Body <br/>
<%= f.text_area :body, :size => "40x6" %>
</p>
<p>
Author <br/>
<%= f.text_field :author %>
</p>
Change new.html.erb so that the form looks like this:
<% form_for(@quote, :url => {:action => "create" }) do |f| %>
<%= render :partial => "form", :locals => { :f => f } %>
<p>
<%= f.submit "Create" %>
</p>
<% end %>
Check that it still works. Do the same for edit.html.erb. Check that it still works.
We still haven’t written the update action. Edit the first quote, and see that clicking on “Update” produces the expected “No action responded to update” message. Define the update method in the controller
def update
@quote = Quote.find(params[:id])
if @quote.update_attributes(params[:quote])
redirect_to :action => "index"
else
render :action => "edit"
end
end
Note how it is similar to the create method.
Finally we get to the “D” in CRUD. Change the index.html.erb view to add this link:
<p>
<%= link_to "Edit", :action => "edit", :id => quote.id %>
| <%= link_to "Destroy",
{:action => "destroy", :id => quote.id },
{:method => "post", :confirm => "Are you sure?"} %>
</p>
We are passing three parameters to link_to: the first is the link text; the second is a hash that describes the url the link points to. The third is a hash that modifies the generated html code.
It is not good to destroy something without confirmation; adding a JavaScript dialog is not the ideal solution but it’s better than nothing. The :confirm => "Are you sure?" clause does just that: it adds a JavaScript confirmation dialog.
It is not good to call destructive actions using the GET http method. This is because proxys, browsers and many web utilities assume that GETs are safe. So we’d like to call our Destroy actions with POST, but it’s not normally possible, in HTML, to define an a element that produces a POST when clicked. The :method => "post" clause solves this dilemma, by adding a JavaScript function that creats an (invisible) form that is invoked when the link is clicked.
Observe the HTML code that is generated by this helper:
<a href="/quotes/destroy/1"
onclick="if (confirm('Are you sure?')) {
var f = document.createElement('form');
f.style.display = 'none';
this.parentNode.appendChild(f);
f.method = 'POST';
f.action = this.href;
var m = document.createElement('input');
m.setAttribute('type', 'hidden');
m.setAttribute('name', '_method');
m.setAttribute('value', 'post');
f.appendChild(m);f.submit(); };
return false;">Delete</a>
This rather messy code is generated automatically by Rails, and it’s a good thing we don’t have to look into it. The Rails helper is much nicer to deal with.
Try the link now: it asks for confirmation, and if the confirmation is given, takes to the expected “No action responded to destroy” method.
Add the destroy action to the controller.
def destroy
Quote.delete(params[:id])
redirect_to :action => "index"
end
And this concludes the kata. We developed six actions: index, new, create, edit, update, destroy. A canonical Rails CRUD contains also a “show” action, that was omitted for brevity.