In this project you’ll create a simple blog system and learn the basics of Ruby on Rails including:
+-
+
- Models, Views, and Controllers (MVC) +
- Data Structures & Relationships +
- Routing +
- Migrations +
- Views with forms, partials, and helpers +
- RESTful design +
- Using Rails plugins/gems +
The project will be developed in five iterations below. The last page, Completed Iterations, has zip files of the complete project after each iteration:
+I0: Up and Running
+Part of the reason Ruby on Rails became popular quickly is that it takes a lot of the hard work off your hands, and that’s especially true in starting up a project. Rails practices the idea of “sensible defaults” and tries to, with one command, create a working application ready for your customization.
+Setting the Stage
+First we need to make sure everything is setup and installed. See the Preparation for Rails Projects page for instructions on setting up and verifying your Ruby, Rails, and add-ons.
+With that done we need to create new project in RubyMine. Open RubyMine and…
+-
+
- Click “Create a New Project”
+
-
+
- Project name: JSBlogger +
- Location: Choose a folder where you want to save your project — and remember it! +
- Project type: Rails application +
- Click OK +
- Ruby Interpreter should be 1.8.7 +
- Rails Version should be 3.0.3 or greater +
- Click
Preconfigure for selected database
and selectSQLite3
+ - Click OK +
+
RubyMine will then create a Rails application for you and automatically open the file database.yml
. If you were connecting to an existing database you would enter the database configuration parameters here. Since we’re using SQLite3 and starting from scratch, we can leave the defaults. Rails will automatically create our database for us.
Then go to the RUN menu in the menubar and click RUN. The first time you do this it’ll display the Run/Debug Configurations window. The defaults are all fine, but I enable the Run Browser
checkbox near the bottom to automatically open the site in my default browser. You can UNCHECK the Display settings before launching
checkbox, if present, then hit RUN.
You should then see the server
window open at the bottom of RubyMine with your Mongrel webserver starting up. Once it’s started RubyMine will open your default browser. If it doesn’t open for some reason, try loading the address http://localhost:3000/. You should see Rails’ “Welcome Aboard” page. Click the “About your application’s environment” link and it’ll display the versions of all your installed components.
Creating the Article Model
+Our blog will be centered around “articles,” so we’ll need a table in the database to store all the articles and a model to allow our Rails app to work with that data. We’ll use one of Rails’ generators to create the required files. Switch to your terminal and enter the following:
++cd JSBlogger +rails generate model Article ++
We’re running the generate
script, telling it to create a model
, and naming that model Article
. From that information, Rails creates the following files:
-
+
/app/models/article.rb
: The file that will hold the model code
+ /test/unit/article_test.rb
: A file to hold unit tests forArticle
+ /test/fixtures/articles.yml
: A fixtures file to assist with unit testing
+ /db/migrate/(some_time_stamp)_create_articles.rb
: A database migration to create thearticles
table
+
With those files in place we can start developing!
+Working with the Database
+Rails uses migration files to perform modifications to the database. Almost any modification you can make to a DB can be done through a migration. The killer feature about Rails migrations is that they’re generally database agnostic. When developing applications I usually use SQLite3 as we are in this tutorial, but when I deploy to my server it is running PostgreSQL. Many others choose MySQL. It doesn’t matter — the same migrations will work on all of them! This is an example of how Rails takes some of the painful work off your hands. You write your migrations once, then run them against almost any database.
+What is a migration? Let’s open /db/migrate/(some_time_stamp)_create_articles.rb
and take a look. First you’ll notice that the filename begins with a mish-mash of numbers which is a timestamp of when the migration was created. Migrations need to be ordered, so the timestamp serves to keep them in chronologic order. Inside the file, you’ll see two methods: self.up
and self.down
.
Migrations are meant to be symmetric. Whatever a migration changes inside the self.up
method should be undone with the self.down
method. Frequently in development you’ll think you want the database to look one way, then realize you need something different. You can create a migration to make your changes to the DB, start building, and then revert as necessary.
Inside the self.up
method you’ll see the generator has placed a call to the create_table
method, passed the symbol :articles
as a parameter, and created a block with the variable t
referencing the table that’s created. We can tell t
what kind of columns we want in the articles
table. Well, what kind of fields does our Article need to have? Since migrations make it easy to add or change columns later, we don’t need to think of EVERYTHING right now, we just need a few to get us rolling. Here’s a starter set:
-
+
title
(a string)
+ body
(a “text”)
+
That’s it! You might be wondering, what is the “text” type? This is an example of relying on the Rails database adapters to make the right call. For some DBs, large text fields are stored as varchar
, while other’s like Postgres use a text
type. The database adapter will figure out the best choice for us depending on the configured database — we don’t have to worry about it.
So add these into your self.up
so it looks like this:
+ def self.up + create_table :articles do |t| + t.string :title + t.text :body + + t.timestamps + end + end ++
What is that t.timestamps
doing there? The generator inserted that line. It will create two columns inside our table titled created_at
and updated_at
. Rails will manage these columns for us, so when an article is created its created_at
and updated_at
are automatically set. Each time we make a change to the article, the updated_at
will automatically be…uhhh…updated. Very handy.
Now our migration is done. You might wonder, what about the self.down
? Didn’t I say migrations need to be symmetric? If we added something to the self.up
it is generally the case that we need to undo that same change in the self.down
. However, when the migration is creating a table, the self.down
can just drop that table regardless of what columns are inside of it. That’s what the generator has setup for us here, where it just says drop_table :articles
.
Save that migration file, switch over to your terminal, and run this command:
++rake db:migrate ++
This command starts the rake
program which is a ruby utility for running maintenance-like functions on your application (working with the DB, executing unit tests, deploying to a server, etc). We tell rake
to db:migrate
which means “look in your set of functions for the database (db
) and run the migrate
function.” The migrate
action finds all migrations in the /db/migrate/
folder, looks at a special table in the DB to determine which migrations have and have not been run yet, then runs any migration that hasn’t been run.
In this case we had just one migration to run and it should print some output like this to your terminal:
++== CreateArticles: migrating ================================================= +-- create_table(:articles) + -> 0.0012s +== CreateArticles: migrated (0.0013s) ======================================== ++
It tells you that it is running the migration named CreateArticles
. And the “migrated” line means that it completed without errors. As I said before, rake keeps track of which migrations have and have not been run. Try running rake db:migrate
again now, and see what happens.
We’ve now created the articles
table in the database and can start working on our Article
model.
Working with a Model in the Console
+Another awesome feature of working with Rails is the console
. The console
is a command-line interface to your application. It allows you to access and work with just about any part of your application directly instead of going through the web interface. This can simplify your development process, and even once an app is in production the console makes it very easy to do bulk modifications, searches, and other data operations. So let’s open the console now by going to your terminal and entering this:
+rails console ++
You’ll then just get back a prompt of >>
. You’re now inside an irb
interpreter with full access to your application. Let’s try some experiments…enter each of these commands one at a time and observe the results:
+puts Time.now +Article.all +Article.new ++
The first line was just to demonstrate that we can do anything we previously did inside irb
now inside of our console
. The second like referenced the Article
model and called the all
method which returns an array of all articles in the database — so far an empty array. The third line created a new article object. You can see that this new object had attributes id
, title
, body
, created_at
, and updated_at
.
All the information about the Article
model is in the file /app/models/article.rb
, so let’s open that now.
Not very impressive, right? There are no attributes defined inside the model, so how does Rails know that an Article should have a title
, a body
, etc? It queries the database, looks at the articles table, and assumes that whatever columns that table has should probably be the attributes accessible through the model.
You created most of those in your migration file, but what about id
? Every table you create with a migration will automatically have an id
column which serves as the table’s primary key. When you want to find a specific article, you’ll look it up in the articles table by its unique ID number. Rails and the database work together to make sure that these IDs are unique, usually using a special column type in the DB like “serial”.
In your console, try entering Article.all
again. Do you see the blank article that we created with the Article.new
command? No? The console doesn’t change values in the database (in most cases) until we explicitly call the .save
method on an object. Let’s create a sample article and you’ll see how it works. Enter each of the following lines one at a time:
+a = Article.new +a.title = "Sample Article Title" +a.body = "This is the text for my article, woo hoo!" +a.save +Article.all ++
Now you’ll see that the Article.all
command gave you back an array holding the one article we created and saved. Go ahead and create 3 more sample articles.
Moving Towards a Web Interface – Setting up the Router
+We’ve created a few articles through the console, but we really don’t have a web application until we have a web interface. Let’s get that started. We said that Rails uses an “MVC” architecture and we’ve worked with the Model, now we need a Controller and View.
+When a Rails server gets a request from a web browser it first goes to the router. The router decides what the request is trying to do, what resources it is trying to interact with. The router dissects a request based on the address it is requesting and other HTTP parameters (like the request type of GET or PUT). Let’s open the router’s configuration file, /config/routes.rb
.
Inside this file you’ll see a LOT of comments that show you different options for routing requests. Let’s remove everything except the first line (ActionController ...
) and the final end
. Then, in between those two lines, add resources :articles
so your file looks like this:
+ActionController::Routing::Routes.draw do |map| + resources :articles +end ++
This line tells Rails to do a lot of work. It declares that we have a resource named articles
and the router should expect requests to follow the RESTful model of web interaction (REpresentational State Transfer). The details don’t matter to you right now, but just know that when you make a request like http://localhost:3000/articles/
the router will know you’re looking for a listing of the articles or http://localhost:3000/articles/new
means you’re trying to create a new article.
Now that the router knows how to handle requests about articles, it needs a place to actually send those requests, the Controller.
+Creating the Articles Controller
+We’re going to use another Rails generator but your terminal has the console currently running. You have two options:
+-
+
- Open a second terminal window (Cmd-T on OS X, open Command Prompt on Win) then CD to your project directory or… +
- Close the console with the command
exit
+
I like to have several terminal windows available to me when developing, so I’d always choose the first option.
+In your terminal, enter this command:
++rails generate controller articles ++
The output shows that the generator created several files/folders for you:
+-
+
app/views/articles
: The directory to contain the controller’s view templates
+ app/controllers/articles_controller.rb
: The controller file itself
+ test/functional/articles_controller_test.rb
: The controller’s unit tests file
+ app/helpers/articles_helper.rb
: A helper file to assist with the views (discussed later)
+ test/unit/helpers/articles_helper_test.rb
: The helper’s unit test file
+
Let’s open up the controller file, /app/controllers/articles_controller.rb
. You’ll see that this is basically a blank class, beginning with the class
keyword and ending with the end
keyword. Any code we add to the controller must go between these two lines, so I like to insert a bunch of blank lines between them so I have room work work and push that final end
farther down the page.
Defining the Index Action
+The first feature we want to add is an “index” page. This is what the app will send back when a user requests http://localhost:3000/articles/
— following the RESTful conventions, this should be a list of the articles. So when the router sees this request come in, it tries to call the index
action inside articles_controller
.
Let’s first try it out by entering http://localhost:3000/articles/
into your web browser. You should get an error message that looks like this:
+Unknown action +No action responded to index. Actions: ++
The router tried to call the index
action, but the articles controller doesn’t have a method with that name. It then lists available actions, but there aren’t any. This is because our controller is still blank. Let’s add the following method inside the controller:
+ def index + @articles = Article.all + end ++
What is that “at” sign doing on the front of @articles
? That marks this variable as an “instance level variable”. We want the list of articles to be accessible from both the controller and the view that we’re about to create. In order for it to be visible in both places it has to be an instance variable. If we had just named it articles
, that local variable would only be available within the index
method of the controller.
Now refresh your browser. The error message changed, but you’ve still got an error, right?
++Template is missing +Missing template articles/index.erb in view path app/views ++
Creating the Index View
+The error message is pretty helpful here. It tells us that the app is looking for a (view) template in /app/views/articles/
but it can’t find one named index.erb
. Rails has assumed that our index
action in the controller should have a corresponding index.erb
view template in the views folder. We didn’t have to put any code in the controller to tell it what view we wanted, Rails just figures it out.
Let’s create that view template now. In the left pane of your RubyMine window, expand the app
folder so you can see views
, then expand views
. Right-click on the articles
folder, select New
then File
and, in the popup, name the file index.html.erb
.
Why did we choose index.html.erb
instead of the index.erb
that the error message said it was looking for? Putting the HTML in the name makes it clear that this view is for generating HTML. In later versions of our blog we might create an RSS feed which would just mean creating an XML view template like index.xml.erb
. Rails is smart enough to pick the right one based on the browser’s request, so when we just ask for http://localhost:3000/articles/
it will find the index.html.erb
and render that file.
Now you’re looking at a blank file. Enter in this view template code which is a mix of HTML and what are called ERB tags:
++<h1>All Articles</h1> + +<ul> + <% @articles.each do |article| %> + <li> + <b><%= article.title %></b><br/> + <%= article.body %> + </li> + <% end %> +</ul> ++
ERB is a templating language that allows us to mix Ruby into our HTML. There are only a few things to know about ERB:
+-
+
- An ERB clause starts with
<%
or<%=
and ends with%>
+ - If the clause started with
<%
, the result of the ruby code will be hidden
+ - If the clause started with
<%=
, the result of the ruby code will be output in place of the clause
+
Save the file and refresh your web browser. You should see a listing of the articles you created in the console. We’ve got the start of a web application!
+I1: Form-based Workflow
+We’ve created articles from the console, but that isn’t a viable long-term solution. The users of our app will expect to add content through a web interface. In this iteration we’ll create an HTML form to submit the article, then all the backend processing to get it into the database.
+Creating the NEW Action and View
+Previously we setup the resources :articles
route in routes.rb
, and that told Rails that we were going to follow the RESTful conventions for this model named Article. Following this convention, the URL for creating a new article would be http://localhost:3000/articles/new
. Enter that into your browser and see what comes up.
+Unknown action +No action responded to new. Actions: index ++
This is an error message we’ve seen before. The router went looking for an action named new
inside the articles_controller
and didn’t find it. For our convenience the message lists the actions that are available — the only one being the index
we created in I0.
So first let’s create that action. Open /app/controllers/articles_controller.rb
and add this method structure, making sure it’s inside the ArticlesController
class, but outside the existing index
method:
+ def new + + end ++
With that defined, refresh your browser and you should get this:
++Template is missing +Missing template articles/new.erb in view path app/views ++
Again, an error message we saw in I0. Create a new file /app/views/articles/new.html.erb
with these contents:
+<h1>Create a New Article</h1> ++
Refresh your browser and you should just see the heading “Create a New Article”.
+Writing a Form
+It’s not very impressive so far — we need to add a form to the new.html.erb
so the user can enter in the article title and body. Because we’re following the RESTful conventions, Rails can take care of many of the details. Inside that erb
file, enter this code below your header:
+<%= form_for(@article) do |f| %> + <p> + <%= f.label :title %><br /> + <%= f.text_field :title %> + </p> + <p> + <%= f.label :body %><br /> + <%= f.text_area :body %> + </p> + <p> + <%= f.submit 'Create' %> + </p> +<% end %> ++
What is all that? Let’s look at it piece by piece:
+-
+
form_for
is a Rails helper method which takes one parameter, in this case@article
and a block with the form fields. The first line basically says “Create a form for the object named@article
, refer to the form by the namef
and add the following elements to the form…”
+ - The
f.label
helper creates an HTML label for a field, this is good usability practice and will have some other benefits for us later
+ - The
f.text_field
helper creates a single-line text box namedtitle
+ - The
f.text_area
helper creates a multi-line text box namedbody
+ - The
f.submit
helper creates a button labeled “Create”
+
Refresh your browser and you’ll see this:
++RuntimeError in Articles#new +Showing app/views/articles/new.html.erb where line #3 raised: +Called id for nil, which would mistakenly be 4 -- if you really wanted the id of nil, use object_id + +Extracted source (around line #3): +1: <h1>Create a New Article</h1> +2: +3: <%= form_for(@article) do |f| %> +4: <%= f.error_messages %> +5: ++
What’s it trying to tell us. In our new.html.erb
on line #3 there was an error about “Called id for nil”. As we learned in the Ruby in 100 minutes tutorial, nil
is Ruby’s way of referring to nothingness. Somewhere in line #3 we’re working with an object that doesn’t exist.
And since there’s only one object in line #3, it makes it pretty obvious — the problem is that we started talking about a thing named @article
without ever creating that thing. Rails uses some of the reflection techniques that we talked about earlier in order to setup the form. Remember in the console when we called Article.new
to see what fields an Article has? Rails wants to do the same thing, but we need to create the blank object for it.
Go into your articles_controller.rb
, and inside the new
method, add this line:
+@article = Article.new ++
Then refresh your browser and your form should come up. Enter in a title, some body text, and click CREATE.
+The CREATE Action
+You’re old friend pops up again…
++Unknown action +No action responded to create. Actions: index and new ++
When we loaded the form we accessed the new
action, but when that form is submitted to the application, following the REST convention, it goes to a create
action. We need to create that action. Inside your articles_controller.rb
add this method (again, inside the ArticlesContoller class, but outside the other methods):
+ def create + @article = Article.new(params[:article]) + @article.save! + redirect_to articles_path + end ++
This method says…
+-
+
- Create an object named
@article
and send in the parameterparams[:article]
+ - Rails makes the form data available inside the variable named
params
. If were to look atparams
as a data structure, it’d be a hash with only one key —:article
. The value of that pair is another hash with keys:title
and:body
. The values for those keys are the data we entered into the text boxes on the form. So whenArticle.new
is called and the hashparams[:article]
is passed in, thenew
method looks for the value with key:title
and puts that into the Article’stitle
attribute. Then it looks for the value for key:body
and puts that into the article’sbody
attribute.
+ - The line
@article.save!
saves the object to the database, just like we did in the console. We’ve added the exclamation mark here because we haven’t implemented any error checking. The normalsave
method will “fail silently” if there’s a problem saving the article, but thesave!
method will raise errors and basically freak out.
+ - Finally, the
redirect_to
tells Rails that we don’t want to render a view for this action. Once the previous steps are done, we want to bounce to the list of all articles. The router generates many friendly path-related variables for us just from the simpleresources :articles
declaration. One of them is thearticles_path
we use here — it’ll resolve to http://localhost:3000/articles/.
+
Go back in your browser so you get to the form with the sample data you entered and click CREATE. You should then bounce to the full articles list with your new article added.
+Adding Navigation to the Index
+Right now our article list is very plain and we end up typing in a bunch of URLs by hand. Let’s add some links. Open your /app/views/articles/index.html.erb
and…
-
+
- Add this code at the very bottom:
<%= link_to "Create a New Article", new_article_path %>
which uses the Railslink_to
helper, tells it we want a link with the text “Create a New Article” that points to the addressnew_article_path
(which the router handles for us)
+ - Find where, in the middle of the view, we output just the
article.title
. Change it so it sayslink_to article.title, article_path(article)
. This creates a link with the text of the articles title which points to a page where we’ll show just that article.
+
Refresh your browser and you should now see a list of just the article titles that are linked somewhere and a link at the bottom to “Create a New Article”. Test that this create link takes you to the new article form. Then go back to the article list and click one of the article titles.
+Creating the SHOW Action
+Tired of this error message yet? Go to your articles_controller.rb
and add a method like this:
+def show + +end ++
Refresh the browser and you’ll get the “Template is Missing” error. Let’s pause here before creating the view template.
+Look at the URL: http://localhost:3000/articles/1
. When we added the link_to
in the index and pointed it to the article_path
for this article
, the router created this URL. Following the RESTful convention, this URL goes to a SHOW method which would display the Article with ID number 1
. Your URL might have a different number depending on which article title you clicked in the index.
So what do we want to do when the user clicks an article title? Find the article, then display a page with its title and body. We’ll use the number on the end of the URL to find the article in the database. The router will send us this number in the variable params[:id]
. Inside the show
method that we just created, add this line:
+@article = Article.find(params[:id]) ++
Now create the file /app/views/articles/show.html.erb
and add this code:
+<h2><%= @article.title %></h2> +<p><%= @article.body %></p> +<%= link_to "<< Back to Articles List", articles_path %> ++
Refresh your browser and your article should show up along with a link back to the index.
+But You Never Make Mistakes!
+We can create articles and we can display them, but when we eventually deliver this to less perfect people than us, they’re going to make mistakes. Right now there’s no way to edit an article once it’s been created. There’s also no way to remove an article. Let’s add those functions.
+Look at your index.html.erb
and change the whole <li>
segment so it looks like this:
+ <li> + <b><%= link_to article.title, article_path(article) %></b><br/> + <i>Actions: + <%= link_to "edit", edit_article_path(article) %>, + <%= link_to "remove", article, :method => :delete, + :confirm => "Remove the article '#{article.title}'?" %> + </i> + </li> ++
The first link we added, for edit, is pretty similar to what we’ve done before — creating a link with the text “edit” pointing to the address edit_article_path
, which is defined by the router, and editing the thing named article
.
The second one is a little more complex. Web browsers don’t yet properly implement all the REST conventions, so Rails creates a hack for destroying objects. The details aren’t too important. So this link will have the text “remove”, will point to the article
, and will use the HTTP method “delete”. We’ve also added a :confirm
parameter. If a link has a :confirm
, then Rails will generate some Javascript which will popup a box when the link is clicked that contains the text in the :confirm
. Here we’re setting the message to check that the user wants to remove the article and including the article’s title in the message.
Refresh your browser and you should see “edit” and “remove” links for each article. Click the EDIT link for your first article.
+Creating an Edit Action & View
+The router is expecting to find an action in articles_controller.rb
named edit
, so let’s add this:
+ def edit + @article = Article.find(params[:id]) + end ++
All the edit
action is really going to do is find the article to be edited, then display the editing form. If you refresh after adding that edit
action you’ll see the template missing error. Create a file /app/views/articles/edit.html.erb
but hold on before you type anything. Below is what the edit form should look like:
+<h1>Edit an Article</h1> + +<%= form_for(@article) do|f| %> + <%= f.error_messages %> + + <p> + <%= f.label :title %><br /> + <%= f.text_field :title %> + </p> + <p> + <%= f.label :body %><br /> + <%= f.text_area :body %> + </p> + <p> + <%= f.submit 'Update' %> + </p> +<% end %> ++
In the Ruby and Rails communities there is a mantra of “Don’t Repeat Yourself” — but that’s exactly what I’ve done here. This view is basically the same as the new.html.erb
— the only changes are the H1 and the name of the button. We can abstract this form into a single file called a partial, then reference this partial from both new.html.erb
and edit.html.erb
.
Create a file /app/views/articles/_form.html.erb
and, yes, it has to have the underscore at the beginning of the filename. Go into your /app/views/articles/new.html.erb
and CUT all the text from and including the form_for
line all the way to its end
. The only thing left will be your H1 line. Then add the following code at the bottom of that view:
+<%= render :partial => 'form' %> ++
Now go back to the _form.html.erb
and paste the form code. Change the text on the submit
button to say “Save” so it makes sense both when creating a new article and editing and existing one.
Then look at your edit.html.erb
file, write an H1 header saying “Edit an Article”, then use the same code to render the partial named form
.
Go back to your articles list and try creating a new article — it should work just fine. Try editing an article and you should see the form with the existing article’s data — it works OK until you click SAVE.
+The router is looking for an action named update
. Just like the new
action sends its form data to the create
action, the edit
action sends its form data to the update
action. In fact, within our articles_controller.rb
, the update
method will look very similar to create
:
+ def update + @article = Article.find(params[:id]) + @article.update_attributes(params[:article]) + @article.save! + redirect_to article_path(@article) + end ++
The only new bit here is the update_attributes
method. This method works very similar to when we called the Article.new
method and passed in the hash of form data. When we call update_attributes
on the @article
object and pass in the data from the form, it changes the values in the object to match the values submitted with the form. Then we save the object to the database and redirect to the articles list.
Now try editing and saving some of your articles.
+Creating a Destroy Action
+Next, click the REMOVE link for one article and hit OK. You can see that the router is expecting there to be a destroy
action. Go into articles_controller.rb
and add a destroy method like this:
+ def destroy + @article = Article.find(params[:id]) + @article.destroy + redirect_to articles_path + end ++
Here we’re doing a find
based on params[:id]
like we did in the show
action. We call that object’s destroy
method, then redirect back to the articles list.
Try it out in your browser.
+Adding a Flash
+It would be nice, though, if we gave the user some kind of status message about the operation that took place. When we create an article the message might say “Article ‘the-article-title’ was created”, or “Article ‘the-article-title’ was removed” for the remove action. We can accomplish this with a special object called the flash
.
Rails creates the object named flash
, so we don’t need to do anything to set it up. We can start by integrating it into our index.html.erb
by adding this line at the very top:
+<div class="flash"><p><%= flash[:message] %></p></div> ++
This just outputs the value stored in the flash
object with the key :message
. If you refresh your articles list you won’t see anything because we haven’t stored a message in there yet. Look at articles_controller.rb
and add this line right after the save!
line in your create
method:
+flash[:message] = "Article '#{@article.title}' was created." ++
Then go to your articles list, create another sample article, and when you click create you should see the flash message at the top of your view.
+Here’s something cool about how Rails handles the flash
— hit your browser’s REFRESH button while looking at the articles list. See how the flash disappears? Once you display the message in a flash Rails clears it out. That’s why it’s perfect for status messages like this.
Similarly, add a flash message into your destroy
method and confirm that it shows up when an article is removed. Then add one to your update
method that’ll display when an article is edited.
And, finally, you’re done with I1!
+I2: Adding Comments
+Most blogs allow the reader to interact with the content by posting comments. Let’s add some simple comment functionality.
+Designing the Comment Model
+First, we need to brainstorm what a comment is…what kinds of data does it have…
+-
+
- It’s attached to an article +
- It has an author name +
- It usually has an author email address +
- It usually has an optional URL +
- It has a body +
With that understanding, let’s create a Comment model. Switch over to your terminal and enter this line:
++rails generate model Comment ++
We’ve already gone through what files this generator creates, we’ll be most interested in the migration file and the comment.rb
.
Setting up the Migration
+Open the migration file that the generator created, /db/migrate/some-timestamp_create_comments.rb
. Inside the self.up
you need to add one line for each of the pieces of data we just brainstormed. It’ll start off with these…
+t.integer :article_id +t.string :author_name ++
Then keep adding lines that create strings named :author_email
, :author_url
, and a text field named :body
.
Once that’s complete, go to your terminal and run the migration with rake db:migrate
.
Relationships
+The power of SQL databases is the ability to express relationships between elements of data. We can join together the information about an order with the information about a customer. Or in our case here, join together an article (in the articles table) with its comments (in the comments table). We do this by using foreign keys.
+Foreign keys are a way of marking one-to-one and one-to-many relationships. An article might have zero, five, or one hundred comments. But a comment only belongs to one article. These objects have a one-to-many relationship — one article connects to many comments.
+Part of the big deal with Rails is that it makes working with these relationships very easy. When we created the migration for comments we started with an integer
field named article_id
. The Rails convention is that, for a one-to-many relationship, the objects on the “many” end should have a foreign key referencing the “one” object. And that foreign key should be titled with the name of the “one” object, then an underscore, then “id”. So in this case one article has many comments, so each comment has a field named article_id
which tracks which article they belong to. Similarly, a store’s customer might have many orders, so each order would have a customer_id
specifying which customer they belong to.
Following this convention will get us a lot of functionality “for free.” Open your /app/models/comment.rb
and add the middle line so it looks like this:
+class Comment < ActiveRecord::Base + belongs_to :article +end ++
A comment relates to a single article, it “belongs to” an article. We then want to declare the other side of the relationship inside /app/models/article.rb
like this:
+class Article < ActiveRecord::Base + has_many :comments +end ++
How an article “has many” comments, and a comment “belongs to” an article. We have explained to Rails that these objects have a one-to-many relationship.
+Testing in the Console
+Let’s use the console to test how this relationship works in code. If you don’t have a console open, go to your terminal and enter rails console
from your project directory. If you have a console open already, enter the command reload!
to refresh any code changes.
Run the following commands one at a time and observe the output:
++a = Article.first +a.comments +Comment.new +a.comments.new ++
When you called the comments
method on object a
, it gave you back a blank array because that article doesn’t have any comments. When you executed Comment.new
it gave you back a blank Comment object with those fields we defined in the migration. But, if you look closely, when you did a.comments.new
the comment object you got back wasn’t quite blank — it has the article_id
field already filled in with the ID number of article a
.
Try creating a few comments for that article like this:
++c = a.comments.new +c.author_name = "Daffy Duck" +c.author_url = "http://daffyduck.com" +c.body = "I think this article is thhh-thhh-thupid!" +c.save +d = a.comments.create(:author_name => "Chewbacca", :body => "RAWR!") ++
For the first comment, c
, I used a series of commands like we’ve done before. For the second comment, d
, I used the create
method. When you use new
it doesn’t go to the database until you call save
. With create
you usually pass in the attributes then the object is created, those attributes set, and the object saved to the database all in one step.
Now that you’ve created a few comments, try executing a.comments
again. Did your comments all show up? When I did it, only one comment came back. The console tries to minimize the number of times it talks to the database, so sometimes if you ask it to do something it’s already done, it’ll get the information from the cache instead of really asking the database — giving you the same answer it gave the first time. That can be annoying. To force it to clear the cache and lookup the accurate information, try this:
+reload! +a = Article.first +a.comments ++
So you’ll see that the article has comments — great. Now we need to integrate them into the article display.
+Displaying Comments for an Article
+We want to display any comments underneath their parent article. Because we’ve setup the relationships between those models, this is very easy. Open /app/views/articles/show.html.erb
and add the following lines right before the link to the articles list:
+<h3>Comments</h3> +<%= render :partial => 'comment', :collection => @article.comments %> ++
This says that we want to render a partial named “comment” and that we want to do it once for each element in the collection @article.comments
. We saw in the console that when we call the .comments
method on an article we’ll get back an array of its associated comment objects. So this render line will pass each element of that array one at a time into the partial named “comment”. Now we need to create the file /app/views/articles/_comment.html.erb
and add this code:
+<div class="comment"> + <h4>Comment by <%=h comment.author_name %></h4> + <p><%=h comment.body %></p> +</div> ++
With that in place, try clicking on your articles and find the one where you created the comments. Did they show up? What happens when an article doesn’t have any comments?
+Web-Based Comment Creation
+Good start, but our users (hopefully) can’t get into the console to create their comments. We’ll need to create a web interface. We’ll go through some of the same steps that we did when creating the web interface for creating articles.
+Let’s start with the form. The comment form should be embedded into the article’s show
template. So let’s add this code right above the “Back to Articles List” in the articles show.html.erb
:
+<%= render :partial => 'comment_form' %> ++
Obviously this is expecting a file /app/views/articles/_comment_form.html.erb
, so create that and add this content for now:
+<h3>Post a Comment</h3> +<p>(Comment form will go here)</p> ++
Look at an article in your browser to make sure that partial is showing up. Then we can start figuring out the details of the form.
+Ok, now look at your articles_controller.rb
in the new
method. Remember how we had to create a blank Article object so Rails could figure out which fields an article has? We need to do the same thing before we create a form for the comment. But when we view the article and display the comment form we’re not running the article’s new
method, we’re running the show
method. So we’ll need to create a blank Comment object inside that show
method like this:
+@comment = @article.comments.new ++
This is just like we did it in the console. Now we can create a form inside our _comment_form.html.erb
partial like this:
+<h3>Post a Comment</h3> + +<%= form_for @comment do |f| %> + <%= f.hidden_field :article_id, :value => @article.id %> + <p> + <%= f.label :author_name %><br/> + <%= f.text_field :author_name %> + </p> + <p> + <%= f.label :author_email %><br/> + <%= f.text_field :author_email %> + </p> + <p> + <%= f.label :author_url %><br/> + <%= f.text_field :author_url %> + </p> + <p> + <%= f.label :body %><br/> + <%= f.text_area :body %> + </p> + <p> + <%= f.submit 'Submit' %> + </p> +<% end %> ++
The only new thing here is the hidden field helper. This hidden field will hold the ID of the article to help when creating the comment object.
+Save then refresh in your web browser and…well…you’ll get an error like this:
++NoMethodError in Articles#show +Showing app/views/articles/_comment_form.html.erb where line #3 raised: +undefined method `comments_path' for #<ActionView::Base:0x10446e510> ++
The form_for
helper is trying to build the form so that it submits to comments_path
, but we haven’t told the router anything about Comments yet. Open /config/routes.rb
and add this line at the top:
+resources :comments ++
Then refresh your browser and your form should show up. Try filling out the comments form and click SUBMIT — you’ll get an error about uninitialized constant CommentsController
.
Creating a Comments Controller
+Just like we needed an articles_controller.rb
to manipulate our Articles, we’ll need a comments_controller.rb
. Switch over to your terminal to generate it with this line:
+rails generate controller comments index create destroy ++
What’s up with those extra parameters? Anything after the name of the controller (in this case “comments”) will cause Rails to create stubs for methods with those names. Now open your /app/controllers/comments_controller.rb
and you’ll see it has the three method stubs already.
The one we’re interested in first is create
. You can cheat by looking at the create
method in your articles_controller.rb
. For your comments_controller.rb
, everything should be the same just replace article with comment. Then the redirect is a little different, use this:
+redirect_to article_path(@comment.article) ++
Test out your form to create another comment now — and it should work!
+Cleaning Up
+We’ve got some decent comment functionality, but there are a few things we should add and tweak.
+Comments Count
+Let’s make it so where the view template has the “Comments” header it displays how many comments there are, like “Comments (3)”. Open up your article’s show.html.erb
and change the comments header so it looks like this:
+<h3>Comments (<%= @article.comments.count %>)</h3> ++
Form Labels
+The comments form looks a little silly with “Author Name” and “Author URL” and such. It should probably say “Your Name” and “Your URL (optional)”, right? To change the text that the label helper prints out, you just pass in the desired text as a second parameter, like this:
++<%= f.label :author_name, "Your Name" %> ++
Change your _comment_form.html.erb
so it prints out “Your Name”, “Your Email Address”, “Your URL (optional)”, and “Your Comment”.
Add Timestamp to the Comment Display
+We should add something about when the comment was posted. Rails has a really neat helper named distance_of_time_in_words
which takes two dates and creates a text description of their difference like “32 minutes later”, “3 months later”, and so on. You can use it in your _comment.html.erb
partial like this:
+<p>Posted <%= distance_of_time_in_words(comment.article.created_at, comment.created_at) %> later</p> ++
With that, you’re done with I2!
+I3: Tagging
+In this iteration we’ll add the ability to tag articles for organization and navigation.
+First we need to think about what a tag is and how it’ll relate to the Article model. If you’re not familiar with tags, they’re commonly used in blogs to assign the article to one or more categories. For instance, if I write an article about a feature in Ruby on Rails, I might want it tagged with all of these categories: ruby, rails, programming, features. That way if one of my readers is looking for more articles about one of those topics they can click on the tag and see a list of my articles with that tag.
+Understanding the Relationship
+What is a tag? We need to figure that out before we can create the model. First, a tag must have a relationship to an article so they can be connected. A single tag, like “ruby” for instance, should be able to relate to many articles. On the other side of the relationship, the article might have multiple tags (like “ruby”, “rails”, and “programming” as above) – so it’s also a many relationship. Articles and tags have a many-to-many relationship.
+Many-to-many relationships are tricky because we’re using an SQL database. If an Article “has many” tags, then we would put the foreign key article_id
inside the tags
table – so then a Tag would “belong to” an Article. But a tag can connect to many articles, not just one. We can’t model this relationship with just the articles
and tags
tables.
When we start thinking about the database modeling, there are a few ways to achieve this setup. One way is to create a “join table” that just tracks which tags are connected to which articles. Traditionally this table would be named articles_tags
and Rails would express the relationships by saying that the Article model has_and_belongs_to_many
Tags, while the Tag model has_and_belongs_to_many
Articles.
Most of the time this isn’t the best way to really model the relationship. The connection between the two models usually has value of its own, so we should promote it to a real model. For our purposes, we’ll introduce a model named “Tagging” which is the connection between Articles and Tags. The relationships will setup like this:
+-
+
- An Article
has_many
Taggings
+ - A Tag
has_many
Taggings
+ - A Tagging
belongs_to
an Article andbelongs_to
a Tag
+
Making Models
+With those relationships in mind, let’s design the new models:
+-
+
- Tag
+
-
+
name
: A string
+
+ - Tagging
+
-
+
tag_id
: Integer holding the foreign key of the related Tag
+ article_id
: Integer holding the foreign key of the related Article
+
+
Note that there are no changes necessary to Article because the foreign key is stored in the Tagging model. So now lets generate these models in your terminal:
++rails generate model Tag name:string +rails generate model Tagging tag_id:integer article_id:integer +rake db:migrate ++
Expressing Relationships
+Now that our model files are generated we need to tell Rails about the relationships between them. For each of the files below, add these lines:
+In /app/models/article.rb
:
+ has_many :taggings ++
In /app/models/tag.rb
:
+ has_many :taggings ++
Then in /app/models/tagging.rb
:
+ belongs_to :article + belongs_to :tag ++
After Rails had been around for awhile, developers were finding this kind of relationship very common. In practical usage, if I had an object named article
and I wanted to find its Tags, I’d have to run code like this:
+tags = article.taggings.collect{|tagging| tagging.tag} ++
That’s a pain for something that we need commonly. The solution was to augment the relationship with “through”. We’ll add a second relationship now to the Article and Tag classes:
+In /app/models/article.rb
:
+ has_many :taggings + has_many :tags, :through => :taggings ++
In /app/models/tag.rb
:
+ has_many :taggings + has_many :articles, :through => :taggings ++
Now if we have an object like article
we can just ask for article.tags
or, conversely, if we have an object named tag
we can ask for tag.articles
.
An Interface for Tagging Articles
+The first interface we’re interested in is within the article itself. When I write an article, I want to have a text box where I can enter a list of zero or more tags separated by commas. When I save the article, my app should associate my article with the tags with those names, creating them if necessary.
+Adding the text field will take place in the file /app/views/articles/_form.html.erb
. Add in a set of paragraph tags underneath the body fields like this:
+ <p> + <%= f.label :tag_list %><br /> + <%= f.text_field :tag_list %> + </p> ++
With that added, try to create an new article in your browser and your should see this error:
++NoMethodError in Articles#new +Showing app/views/articles/_form.html.erb where line #14 raised: +undefined method `tag_list' for #<Article:0x10499bab0> ++
An Article doesn’t have a thing named tag_list
— we made it up. In order for the form to display, we need to add a method to the article.rb
file like this:
+ def tag_list + return self.tags.join(", ") + end ++
Your form should now show up and there’s a text box at the bottom named “Tag list”. Enter content for another sample article and in the tag list enter ruby, technology
. Click SAVE and you’ll get an error like this:
+ActiveRecord::UnknownAttributeError in ArticlesController#create +unknown attribute: tag_list ++
What is this all about? Let’s start by looking at the form data that was posted when we clicked SAVE. This data is in the production.log file which should be in the “Console” frame at the bottom of the RubyMine window. Look for the line that starts “Processing ArticlesController#create”, here’s what mine looks like:
++Processing ArticlesController#create (for 127.0.0.1) [POST] + Parameters: {"article"=>{"body"=>"Yes, the samples continue!", "title"=>"My Sample", "tag_list"=>"ruby, technology"}, "commit"=>"Save", "authenticity_token"=>"xxi0A3tZtoCUDeoTASi6Xx39wpnHt1QW/6Z1jxCMOm8="} ++
The field that’s interesting there is the "tag_list"=>"technology, ruby"
. Those are the tags as I typed them into the form. The error came up in the create
method, so let’s peek at /app/controllers/articles_controller.rb
in the create
method. See the first line that calls Article.new(params[:article])
? This is the line that’s causing the error as you could see in the middle of the stack trace.
Since the create
method passes all the parameters from the form into the Article.new
method, the tags are sent in as the string "technology, ruby"
. The new
method will try to set the new Article’s tag_list
equal to "technology, ruby"
but that method doesn’t exist because there is no attribute named tag_list
.
There are several ways to solve this problem, but the simplest is to pretend like we have an attribute named tag_list
. We can define the tag_list=
method inside article.rb
like this:
+ def tag_list=(tags_string) + + end ++
Just leave it blank for now and try to resubmit your sample article with tags. It goes through!
+Not So Fast
+Did it really work? It’s hard to tell. Let’s jump into the console and have a look.
++a = Article.last +a.tags ++
I bet the console reported that a
had []
tags — an empty list. So we didn’t generate an error, but we didn’t create any tags either.
We need to return to that tag_list=
method in article.rb
and do some more work. We’re taking in a parameter, a string like "tag1, tag2, tag3"
and we need to associate the article with tags that have those names. The pseudo-code would look like this:
-
+
- Cut the parameter into a list of strings with leading and trailing whitespace removed (so
"tag1, tag2, tag3"
would become["tag1","tag2","tag3"]
+ - For each of those strings…
+
-
+
- Look for a Tag object with that name. If there isn’t one, create it. +
- Create a Tagging object that connects this Article with that Tag +
+
The first step is something that Ruby does very easily using the .split
method. Go into your console and try "tag1, tag2, tag3".split
. By default it split on the space character, but that’s not what we want. You can force split to work on any character by passing it in as a parameter, like this: "tag1, tag2, tag3".split(",")
.
Look closely at the output and you’ll see that the second element is " tag2"
instead of "tag2"
— it has a leading space. We don’t want our tag system to end up with different tags because of some extra (non-meaningful) spaces, so we need to get rid of that. Ruby’s String class has a strip
method that pulls off leading or trailing whitespace — try it with " my sample ".strip
. You’ll see that the space in the center is preserved.
So to combine that with our strip
, try this code:
+"tag1, tag2, tag3".split(",").collect{|s| s.strip.downcase} ++
The .split(",")
will create the list with extra spaces as before, then the .collect
will take each element of that list and send it into the following block where the string is named s
and the strip
and downcase
methods are called on it. The downcase
method is to make sure that “ruby” and “Ruby” don’t end up as different tags. This line should give you back ["tag1", "tag2", "tag3"]
.
Now, back inside our tag_list=
method, let’s add this line:
+tag_names = tags_string.split(",").collect{|s| s.strip} ++
So looking at our pseudo-code, the next step is to go through each
of those tag_names
and find or create a tag with that name. Rails has a built in method to do just that, like this:
+tag = Tag.find_or_create_by_name(tag_name) ++
Once we find or create the tag
, we need to create a tagging
which connects this article (here self
) to the tag like this:
+self.taggings.build(:tag => tag) ++
The build
method is a special creation method. It doesn’t need an explicit save, Rails will wait to save the Tagging until the Article itself it saved. So, putting these pieces together, your tag_list=
method should look like this:
+ def tag_list=(tags_string) + tag_names = tags_string.split(",").collect{|s| s.strip.downcase} + tag_names.each do |tag_name| + tag = Tag.find_or_create_by_name(tag_name) + self.taggings.build(:tag => tag) + end + end ++
Testing in the Console
+Go back to your console and try these commands:
++reload! +a = Article.new(:title => "A Sample Article for Tagging!",:body => "Great article goes here", :tag_list => "ruby, technology") +a.save +a.tags ++
You should get back a list of the two tags. If you’d like to check the other side of the Article-Tagging-Tag relationship, try this:
++t = a.tags.first +t.articles ++
And you’ll see that this Tag is associated with just one Article.
+Adding Tags to our Display
+According to our work in the console, articles can now have tags, but we haven’t done anything to display them in the article pages. Let’s start with /app/views/articles/show.html.erb
. Right below the line that displays the article.title
, add this line:
+Tags: <%= tag_links(@article.tags) %><br /> ++
This line calls a helper named tag_links
and sends the article.tags
array as a parameter. We need to then create the tag_links
helper. Open up /app/helpers/articles_helper.rb
and add this method inside the module
/end
keywords:
+def tag_links(tags) + +end ++
The desired outcome is a list of comma separated tags, where each one links to that tag’s show
action — the page where we’ll list all the articles with that tag.
A helper method has to return a string which will get rendered into the HTML. In this case we’ll use the collect
method to create a list of links, one for each Tag, where the link is created by the link_to
helper. Then we’ll return
back the links
connected by a comma and a space:
+ def tag_links(tags) + links = tags.collect{|tag| link_to tag.name, tag_path(tag)} + return links.join(", ") + end ++
Refresh your view and…BOOM:
++NoMethodError in Articles#show +Showing app/views/articles/index.html.erb where line #6 raised: +undefined method `tag_path' for #<ActionView::Base:0x104aaa460> ++
The link_to
helper is trying to use tag_path
from the router, but the router doesn’t know anything about our Tag object. We created a model, but we never created a controller or route. There’s nothing to link to — so let’s generate that controller from your terminal:
+rails generate controller tags index show ++
Then we need to add it to our /config/routes.rb
like this:
+resources :tags ++
Now refresh your view and you should see your linked tags showing up on the individual article pages.
+Lastly, use similar code in /app/views/articles/index.html.erb
to display the tags on the article listing page.
Avoiding Repeated Tags
+Try editing one of your article that already has some tags. Save it and look at your article list. You’ll probably see that tags are getting repeated, which is obviously not what we want.
+When we wrote our tag_list=
method inside of article.rb
, we were just thinking about it running when creating a new article. Thus we always built a new tagging for each tag in the list. But when we’re editing, we might get the string “ruby, technology” into the method while the Article was already linked to the tags “ruby” and “technology” when it was created. As it is currently written, the method will just “retag” it with those tags, so we’ll end up with a list like “ruby, technology, ruby, technology”.
There are a few ways we could fix this — the first thing I want to do is remove any repeated tags in the parameter list by using the Ruby method uniq
:
+tag_names = tags_string.split(",").collect{|s| s.strip.downcase}.uniq ++
This is a good start but it doesn’t solve everything. We’d still get repeated tags each time we edit an article.
+If we edit an article and remove a tag from the list, this method as it stands now isn’t going to do anything about it. Since we don’t have anything valuable in the Tagging object besides the connection to the article and tag, they’re disposible. We can just destroy all the taggings at the beginning of the method. Any tags that aren’t in the tags_string
won’t get re-linked. This will both avoid removed tags and prevent the “double tagging” behavior. Putting that all together, here’s my final tag_list=
method:
+ def tag_list=(tags_string) + self.taggings.destroy_all + tag_names = tags_string.split(",").collect{|s| s.strip.downcase}.uniq + tag_names.each do |tag_name| + tag = Tag.find_or_create_by_name(tag_name) + self.taggings.build(:tag => tag) + end + end ++
It prevents duplicates and allows you to remove tags from the edit form. Test it out and make sure things are working!
+Listing Articles by Tag
+The links for our tags are showing up, but if you click on them you’ll get our old friend, the “No action responded to show. Actions:” error. Open up your /app/controllers/tags_controller.rb
and add a a show
method like this:
+ def show + @tag = Tag.find(params[:id]) + end ++
Then create a file /app/views/tags/show.html.erb
like this:
+<h1>Articles Tagged with <%= @tag.name %></h1> + +<ul> + <% @tag.articles.each do |article| %> + <li><%= link_to article.title, article_path(article) %></li> + <% end %> +</ul> ++
Refresh your view and you should see a list of articles with that tag. Keep in mind that there might be some abnormalities from articles we tagged before doing our fixes to the tag_list=
method. For any article with issues, try going to its edit
screen, saving it, and things should be fixed up. If you wanted to clear out all taggings you could do Tagging.destroy_all
from your console.
Listing All Tags
+We’ve built the show
action, but the reader should also be able to browse the tags available at http://localhost:3000/tags/
. I think you can do this on your own. Create an index
action in your tags_controller.rb
and an index.html.erb
in the corresponding views folder. Look at your articles_controller.rb
and Article index.html.erb
if you need some clues.
If that’s easy, try creating a destroy
method in your tags_controller.rb
and adding a destroy link to the tag list. If you do this, change the association in your tag.rb
so that it says has_many :taggings, :dependent => :destroy
. That’ll prevent orphaned Tagging objects from hanging around.
With that, a long Iteration 3 is complete!
+I4: Installing Plugins
+In this iteration we’ll learn how to take advantage of the many plugins and libraries available to quickly add features to your application. First we’ll work with paperclip, a library that manages file attachments and uploading.
+Using the Gemfile to Setup a RubyGem
+In the past Rails plugins were distributed a zip or tar files that got stored into your application’s file structure. One advantage of this method is that the plugin could be easily checked into your source control system along with everything you wrote in the app. The disadvantage is that it made upgrading to newer versions of the plugin, and dealing with the versions at all, complicated.
+Most Rails plugins are now moving toward RubyGems. RubyGems is a package management system for Ruby, similar to how Linux distributions use Apt or RPM. There are central servers that host libraries, and we can install those libraries on our machine with a single command. RubyGems takes care of any dependencies, allows us to pick an options if necessary, and installs the library.
+Let’s see it in action. If you have your server running in RubyMine, click the red square button to STOP it. If you have a console session open, type exit
to exit. Then open up /Gemfile
and look for the lines like this:
+ # gem 'bj' + # gem 'nokogiri' + # gem 'sqlite3-ruby', :require => 'sqlite3' ++
These lines are commented out because they start with the #
character. By specifying a RubyGem with the gem
command, we’ll tell the Rails application “Make sure this gem is loaded when you start up. If it isn’t available, freak out!” Here’s how we’ll require the paperclip gem, add this near those commented lines:
+ gem "paperclip" ++
When you’re writing a production application, you might specify additional parameters that require a specific version or a custom source for the library. With that config line declared, click the green arrow in RubyMine to startup your server. You should get an error like this:
++ Could not find gem 'paperclip (>= 0, runtime)' in any of the gem sources listed in your Gemfile. + Try running `bundle install`. ++
The last line is key — since our config file is specifying which gems it needs, the bundle
command can help us install those gems. Go to your terminal and:
+bundle ++
It should then install the paperclip RubyGem with a version like 2.3.8. In some projects I work on, the config file specifies upwards of 18 gems. With that one bundle
command the app will check that all required gems are installed with the right version, and if not, install them.
Now we can start using the library in our application!
+Setting up the Database for Paperclip
+We want to add images to our articles. To keep it simple, we’ll say that a single article could have zero or one images. In later versions of the app maybe we’d add the ability to upload multiple images and appear at different places in the article, but for now the one will show us how to work with paperclip.
+First we need to add some fields to the Article model that will hold the information about the uploaded image. Any time we want to make a change to the database we’ll need a migration. Go to your terminal and execute this:
++rails generate migration add_paperclip_fields_to_article ++
That will create a file in your /db/migrate/
folder that ends in _add_paperclip_fields_to_article.rb
. Open that file now.
Remember that the code inside the self.up
method is to migrate the database forward, while the self.down
should undo those changes. We’ll use the add_column
and remove_column
methods to setup the fields paperclip is expecting:
+class AddPaperclipFieldsToArticle < ActiveRecord::Migration + def self.up + add_column :articles, :image_file_name, :string + add_column :articles, :image_content_type, :string + add_column :articles, :image_file_size, :integer + add_column :articles, :image_updated_at, :datetime + end + + def self.down + remove_column :articles, :image_file_name + remove_column :articles, :image_content_type + remove_column :articles, :image_file_size + remove_column :articles, :image_updated_at + end +end ++
The go to your terminal and run rake db:migrate
. The rake command should show you that the migration ran and added columns to the database.
Adding to the Model
+The gem is loaded, the database is ready, but we need to tell our Rails application about the image attachment we want to add. Open /app/models/article.rb
and just below the existing has_many
lines, add this line:
+has_attached_file :image ++
This has_attached_file
method is part of the paperclip library. With that declaration, paperclip will understand that this model should accept a file attachment and that there are fields to store information about that file which start with image_
in this model’s database table.
Modifying the Form Template
+First we’ll add the ability to upload the file when editing the article, then we’ll add the image display to the article show template. Open your /app/views/articles/_form.html.erb
view template. We need to make two changes…
In the very first line, we need to specify that this form needs to accept “multipart” data. This is an instruction to the browser about how to submit the form. Change your top line so it looks like this:
++<% form_for(@article, :html => {:multipart => true}) do |f| %> ++
Then further down the form, right before the paragraph with the save button, let’s add a label and field for the file uploading:
++ <p> + <%= f.label :image, "Attach an Image" %><br /> + <%= f.file_field :image %> + </p> ++
Trying it Out
+If your server isn’t running, start it up with the green play button in RubyMine. Then go to http://localhost:3000/articles/
and click EDIT for your first article. The file field should show up towards the bottom. Click the Choose a File
and select one of the small images that I’ve distributed to you. Click SAVE and you’ll return to the article index. Click the title of the article you just modified. What do you see? Did the image attach to the article?
When I first did this, I wasn’t sure it worked. Here’s how I checked:
+-
+
- Open a console session (
rails console
from terminal)
+ - Find the ID number of the article by looking at the URL. In my case, the url was
http://localhost:3000/articles/1
so the ID number is just1
+ - In console, enter
a = Article.find(1)
+ - Right away I see that the article has data in the
image_file_name
and other fields, so I think it worked.
+ - Enter
a.image
to see even more data about the file
+
Ok, it’s in there, but we need it to actually show up in the article. Open the /app/views/articles/show.html.erb
view template. In between the line that displays the title and the one that displays the body, let’s add this line:
+<%= image_tag @article.image.url %> ++
Then refresh the article in your browser. Tada!
+Improving the Form
+When first working with the edit form I wasn’t sure the upload was working because I expected the file_field
to display the name of the file that I had already uploaded. Go back to the edit screen in your browser for the article you’ve been working with. See how it just says “Choose File, no file selected” — nothing tells the user that a file already exists for this article. Let’s add that information in now.
So open that /app/views/articles/_form.html.erb
and look at the paragraph where we added the image upload field. We’ll add in some new logic that works like this:
-
+
- If the article has an image filename
+
-
+
- Display the image +
+ - Then display the
file_field
button with the label “Attach a New Image”
+
So, turning that into code…
++ <p> + <% if @article.image_file_name %> + <%= image_tag @article.image.url %><br/> + <% end %> + <%= f.label :image, "Attach a New Image" %><br /> + <%= f.file_field :image %> + </p> ++
Test how that looks both for articles that already have an image and ones that don’t.
+When you “show” an article that doesn’t have an image attached it’ll have an ugly broken link. Go into your /app/views/articles/show.html.erb
and add a condition like we did in the form so the image is only displayed if it actually exists.
Now our articles can have an image and all the hard work was handled by paperclip!
+Further Notes about Paperclip
+Yes, a model (in our case an article) could have many attachments instead of just one. To accomplish this you’d create a new model, let’s call it “Attachment”, where each instance of the model can have one file using the same fields we put into Article above as well as an article_id
field. The Attachment would then belong_to
an article, and an article would have_many
attachments.
Paperclip supports automatic image resizing and it’s easy. In your model, you’d add an option like this:
++as_attached_file :image, :styles => { :medium => "300x300>", :thumb => "100x100>" } ++
This would automatically create a “medium” size where the largest dimension is 300 pixels and a “thumb” size where the largest dimension is 100 pixels. Then in your view, to display a specific version, you just pass in an extra parameter like this:
++<%= image_tag @article.image.url(:medium) %> ++
If it’s so easy, why don’t we do it right now? The catch is that paperclip doesn’t do the image manipulation itself, it relies on a package called imagemagick. Image processing libraries like this are notoriously difficult to install. If you’re on Linux, it might be as simple as sudo apt-get install imagemagick
. On OS X, if you have Mac Ports installed, it’d just be sudo port install imagemagick
. On windows you need to download an copy some EXEs and DLLs. It can be a hassle, which is why we won’t do it during this class.
Installing HAML/SASS
+Another plugin that I use in every project is actually two libraries in one. HAML is an alternative templating style to the default ERB (which you’ve been using, hence all the view templates ending in .erb
). SASS is a library for writing CSS and it makes CSS much, much easier to work with.
Open your Gemfile
and add a gem
line for the gem haml
. Go to your terminal and bundle
and it should pull down the gem library for you. Stop (with the red square) then restart (green play button) your server within RubyMine. Both HAML and SASS are installed and ready to use.
Look in RubyMine’s left navigation pane for the folder /public/stylesheets/
. Right click on this folder, click NEW, then DIRECTORY, and name it sass
. Then right click on the sass
folder, click NEW, FILE, then enter the name styles.sass
A Few Sass Examples
+All the details about Sass can be found here: http://sass-lang.com/
+We’re not focusing on CSS development, so here are a few styles that you can copy & paste and modify to your heart’s content:
++!primary_color = #AAA + +body + :background-color = !primary_color + :font + :family Verdana, Helvetica, Arial + :size 14px + +a + :color #0000FF + img + :border none + +.clear + :clear both + :height 0 + :overflow hidden + +#container + :width 75% + :margin 0 auto + :background #fff + :padding 20px 40px + :border solid 1px black + :margin-top 20px + +#content + :clear both + :padding-top 20px ++
But our application isn’t setup to load that stylesheet yet. We need to make a change to our view templates.
+Working with Layouts
+We’ve created about a dozen view templates between our different models. We could go into each of those templates and add a line like this at the top:
++<%= stylesheet_link_tag 'styles' %> ++
Which would find the Sass file we just wrote. That’s a lame job, imagine if we had 100 view templates. What if we want to change the name of the stylesheet later? Ugh.
+Rails and Ruby both emphasize the idea of “D.R.Y.” — Don’t Repeat Yourself. In the area of view templates, we can achieve this by creating a layout. A layout is a special view template that wraps other views. Look in your navigation pane for /app/views/layouts/
, right click on that folder, click NEW and FILE then give it the name application.html.haml
.
In this layout we’ll put the view code that we want to render for every view template in the application. Just so you can see what HAML looks like, I’ve used it to implement this layout. You’ll notice that HAML uses fewer marking characters than ERB, but you must maintain the proper whitespace/indentation. All indentations are two spaces from the containing element. Add this code to your application.html.haml
:
+!!! Strict +%html + %head + %title + JSBlogger + = stylesheet_link_tag 'styles' + + %body + #container + #content + = yield ++
Now refresh your article listing page and you should see the styles take effect. Whatever code is in the individual view template gets inserted into the layout where you see the yield
. Using layouts makes it easy to add site-wide elements like navigation, sidebars, and so forth.
NOTE: If you don’t see any change, look at your server log in RubyMine to see if there were any errors. At first I had a typo in one of the filenames so it wasn’t being picked up properly. You might also need to stop & restart your server if you didn’t do that after installing the haml
gem.
Now that you’ve tried out three plugin libraries (Paperclip, HAML, and SASS), Iteration 4 is complete!
+I5: Authentication
+Authentication is an important part of almost any web application and there are several approaches to take. Thankfully some of these have been put together in plugins so we don’t have to reinvent the wheel.
+The “flavor-of-the-week” is one named AuthLogic and I wrote up an iteration using it for the JSMerchant tutorial, but I think it is a little complicated for a Rails novice. You have to create several different models, controllers, and views manually. The documentation is kind of confusing, and I don’t think my tutorial is that much better.
+So, instead, we’ll use the most common authentication plugin, restful_authentication.
+Installing restful_authentication
+In the previous iteration we installed libraries using RubyGems. This is the preferred way of managing plugins, but restful_authentication isn’t available through RubyGems. We’ll have to use the older method of installing a plugin.
+At your terminal, enter the following:
++ruby git clone git://github.com/technoweenie/restful-authentication.git restful_authentication ++
If you aren’t able to pull down the code from GitHub, you’ll need to download the code in a zip file from github, then extract the zip into /vendor/plugins/restful_authentication/
.
Once you’ve installed the plugin, you can test that it’s available with this command at your terminal:
++rails generate ++
In the output is a section title “Installed Generators”. It should have a line that says Plugins (vendor/plugins): authenticated
. If it’s there, you’re ready to go!
Running the Generator
+This plugin makes it easy to get up an running because one generator creates most of the code you’ll need. Run this from your terminal:
++rails generate authenticated user sessions ++
Take a look at the output and you’ll see it created about 16 files and added a few routes to the routes.rb
file. The generator has a note that you shouldn’t forget to add the routes to routes.rb
, but it’s already done that for you.
Let’s look at the CreateUsers migration that the generator created before we migrate the database. If you wanted your User models to have any additional information (like “deparment_name” or “favorite_color”) you could add columns for that. For our purposes these fields look alright and, thanks to the flexibility of migrations, if we want to add columns later it’s easy. So go to your terminal and enter:
++rake db:migrate ++
Creating a First Account
+First, stop then restart your server in RubyMine to make sure it’s picked up the plugin. Then go to http://localhost:3000/users/new
and the new user form should popup.
Go ahead and create yourself an account. If you’re successful it will just bounce you to the http://localhost:3000/
root page. There isn’t any information about our login status in our views, though, so it’s hard to tell if we’re really logged in.
Let’s open /app/views/layouts/application.html.haml
and add a little footer so the whole %body%
chunk looks like this:
+ %body + #container + #content + = yield + %hr + %h6 + - if current_user + = "Logged in as #{current_user.login}" + = link_to "(logout)", logout_path + - else + = link_to "(login)", login_path ++
The go to http://localhost:3000/articles/
and you’ll get this error:
+NameError in Articles#index +Showing app/views/layouts/application.html.haml where line #14 raised: +undefined local variable or method `current_user' for #<ActionView::Base:0x103147400> ++
We tried to use the current_user
helper that comes with the restful_authentication plugin, but Rails isn’t recognizing it. We need to do one more setup step. Open /app/controllers/application_controller.rb
, and underneath the protect_from_forgery
line, add this: include AuthenticatedSystem
.
Now refresh your browser and your articles list should come up along with the new footer at the bottom.
+An Aside on the Site Root
+It’s annoying me that we keep going to http://localhost:3000/
and seeing the Rails starter page. Let’s make the root show our articles index page.
First, delete the file /public/index.html
. Files in the public directory will take precedence over routes in our application, so as long as that file exists we can’t route the root address anywhere.
Second, open /config/routes.rb
and right above the other routes add in this one:
+match '/' => 'articles#index' ++
Now visit http://localhost:3000
and you should see your article list.
Securing New Users
+It looks like we can create a new user, but right away I want to make some changes. We’re just going to use one layer of security for the app — a user who is logged in has access to all the commands and pages, while a user who isn’t logged in can only post comments and try to login. But that scheme will breakdown if just anyone can go to this URL and create an account, right?
+Let’s add in a protection scheme like this to the new users form:
+-
+
- If there are zero users in the system, let anyone access the form +
- If there are more than zero users registered, only users already logged in can access this form +
That way when the app is first setup we can create an account, then new users can only be created by a logged in user.
+We can create a before_filter
which will run before the new
and create
actions of our users_controller.rb
. Open that controller and on the second line you’ll see include AuthenticatedSystem
. You can remove that since we put it in the application_controller
and, in its place, put all this code:
+ before_filter :zero_users_or_authenticated, :only => [:new, :create] + + def zero_users_or_authenticated + unless User.all.size == 0 || current_user + redirect_to root_path + return false + end + end ++
The first line declares that we want to run a before filter named zero_or_authenticated
when either the new
or create
methods are accessed. Then we define that filter, checking if there are either zero registered users OR if there is a user already logged in. If neither of those is true, we redirect to the root path (our articles list) and return false. If either one of them is true this filter won’t do anything, allowing the requested user registration form to be rendered.
With that in place, try accessing /users/new
when you logged in and when your logged out. If you want to test that it works when no users exist, try this at your console:
+User.destroy_all ++
Then try to reach the registration form and it should work! Create yourself an account if you’ve destroyed it.
+Securing the Rest of the Application
+The first thing we need to do is sprinkle before_filters
on most of our controllers:
-
+
- In
users_controller
, add a before filter to protect the actions besidesnew
andcreate
like this:before_filter :login_required, :except => [:new, :create]
+ - In
tags_controller
, we don’t have any methods that need to be protected.
+ - In
sessions_controller
all the methods need to be accessible to allow login and logout
+ - In
comments_controller
, we never implementedindex
anddestroy
, but just in case we do let’s allow unauthenticated users to only accesscreate
:before_filter :login_required, :except => [:create]
+ - In
articles_controller
authentication should be required fornew
,create
,edit
,update
anddestroy
. Figure out how to write the before filter using either:only
or:except
+
Now our app is pretty secure, but we should hide all those edit, destroy, and new article links from unauthenticated users.
+Open /app/views/articles/index.html.erb
and find the section where we output the “Actions”. Wrap that whole section in an if
clause like this:
+<% if current_user %> + +<% end %> ++
Look at the article listing in your browser when you’re logged out and make sure those links disappear. Then use the same technique to hide the “Create a New Article” link.
+If you look at the show
view template, you’ll see that we never added an edit link! Add that link now, but protect it to only show up when a user is logged in.
Your basic authentication is done, and Iteration 5 is complete!
+
I6: Extras
+Here are some ideas for extension exercises:
+-
+
- Add a site-wide sidebar that holds navigation links +
- Create date-based navigation links. For instance, there would be a list of links with the names of the months and when you click on the month it shows you all the articles published in that month. +
- Track the number of times an article has been viewed. Add a
view_count
column to the article, then in theshow
method ofarticles_controller.rb
just increment that counter. Or, better yet, add a method in thearticle.rb
model that increments the counter and call that method from the controller.
+ - Once you are tracking views, create a list of the three “most popular” articles +
- Create a simple RSS feed for articles using the
respond_to
method and XML view templates
+