my face
About Me

Published Posts

All Posts

New Post


View by Tag:

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


FeedBurner picture


Online Portfolio

Resume

Profile on LinkedIn

Recommend Me

Building Refactor It, a Rails3 App, in Steps

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

Part Two: Nested Resources

The idea of "Refactor It" is that users can submit a snippet, and other users can suggest refactorings.

That suggests a simple data architecture: snippet objects contain refactoring objects, and they're all represented by simple ActiveRecord models. So let's do that.

It'll be quicker to build these things with scaffolds. Rails 3 generators use the "rails generate" command, which has nice help:

   1  angelbob@dell-desktop:~/src/rails/refactor$ rails generate scaffold 
   2  Usage:
   3    rails generate scaffold NAME [field:type field:type] [options]
   4  
   5  Options:
   6        [--singleton]               # Supply to create a singleton controller
   7    -c, --scaffold-controller=NAME  # Scaffold controller to be invoked
   8                                    # Default: scaffold_controller
   9    -y, [--stylesheets]             # Indicates when to generate stylesheets
  10                                    # Default: true
  11        [--force-plural]            # Forces the use of a plural ModelName
  12    -o, --orm=NAME                  # Orm to be invoked
  13                                    # Default: active_record
  14  
  15  ScaffoldController options:
  16    -t, [--test-framework=NAME]   # Test framework to be invoked
  17                                  # Default: test_unit
  18    -e, [--template-engine=NAME]  # Template engine to be invoked
  19                                  # Default: erb
  20        [--helper]                # Indicates when to generate helper
  21                                  # Default: true
  22  
  23  Runtime options:
  24    -q, [--quiet]    # Supress status output
  25    -s, [--skip]     # Skip files that already exist
  26    -p, [--pretend]  # Run but do not make any changes
  27    -f, [--force]    # Overwrite files that already exist
  28  
  29  TestUnit options:
  30        [--fixture]                   # Indicates when to generate fixture
  31                                      # Default: true
  32    -r, [--fixture-replacement=NAME]  # Fixture replacement to be invoked
  33  
  34  ActiveRecord options:
  35    [--migration]      # Indicates when to generate migration
  36                       # Default: true
  37    [--parent=PARENT]  # The parent class for the generated model
  38    [--timestamps]     # Indicates when to generate timestamps
  39                       # Default: true
  40  
  41  Description:
  42      Create rails files for scaffold generator.

From there, we can generate the snippet and refactor objects:

   1  angelbob@dell-desktop:~/src/rails/refactor$ rails generate scaffold snippet title:string body:text notes:text language:string user_id:integer karma:integer
   2        invoke  active_record
   3        create    db/migrate/20100701022620_create_snippets.rb
   4        create    app/models/snippet.rb
   5        invoke    test_unit
   6        (Cut for length...)
   7        create        test/unit/helpers/snippets_helper_test.rb
   8        invoke  stylesheets
   9        create    public/stylesheets/scaffold.css
  10  angelbob@dell-desktop:~/src/rails/refactor$ rails generate scaffold refactoring body:text comment:text snippet_id:integer language:string user_id:integer karma:integer
  11        invoke  active_record
  12        create    db/migrate/20100701030608_create_refactorings.rb
  13        create    app/models/refactoring.rb
  14        invoke    test_unit
  15        (Cut for length...)
  16        create        test/unit/helpers/refactorings_helper_test.rb
  17        invoke  stylesheets
  18     identical    public/stylesheets/scaffold.css

That's fine -- let's look at the migrations that Rails set up for us. Here's the one for snippets, after I've added a couple of options on data fields:

   1  class CreateSnippets < ActiveRecord::Migration
   2    def self.up
   3      create_table :snippets do |t|
   4        t.string :title
   5        t.text :body
   6        t.text :notes
   7        t.string :language
   8        t.integer :user_id, :null => false
   9        t.integer :karma, :default => 0
  10  
  11        t.timestamps
  12      end
  13    end
  14  
  15    def self.down
  16      drop_table :snippets
  17    end
  18  end

Looks good. I also set the snippet_id to not null in the refactoring model, and the karma to default to 0. You can do that too.

Now we have a couple of nice models for this, and a simple scaffolding to create and edit each of them. Open up config/routes.rb, and you'll see "resources" lines for each of them. You'll want to change the sequential lines into a very simple nested block:

   1    resources :snippets do
   2      resources :refactorings
   3    end

Now refactorings will be nested inside snippets. Not in the database, of course, but the URLs will be written that way. Let's have a look. Run your app by typing "rails server" in your app's directory. Open a web browser to "http://localhost:3000/snippets".

Looks great, doesn't it? No? You have an error, instead? One like this?

An error!

I guess we need to actually run those migrations. Let's do that.

   1  angelbob@dell-desktop:~/src/rails/refactor$ rake db:migrate
   2  (in /home/angelbob/src/rails/refactor)
   3  ==  CreateSnippets: migrating =================================================
   4  -- create_table(:snippets)
   5     -> 0.0019s
   6  ==  CreateSnippets: migrated (0.0020s) ========================================
   7  
   8  ==  CreateRefactorings: migrating =============================================
   9  -- create_table(:refactorings)
  10     -> 0.0018s
  11  ==  CreateRefactorings: migrated (0.0019s) ====================================

Now hit reload in the browser. Better?

a listing of snippets

Excellent. Now we can create a new snippet. Right now there's no security on anything, so don't worry about the user and login stuff we did last time.

editing a new snippet

And now we've created it...

created a new snippet

We're now viewing the new snippet. Look at the URL: "http://localhost:3000/snippets/1". Go ahead and change that to "http://localhost:3000/snippets/1/refactorings". Got an error like this?

no such method error

Now we'll see what we need to do to make nested resources work. First off, we can't just use a "new_refactoring_path" like the scaffold tried to do. We have to specify a snippet as well, since that's required to get the path to a refactoring. And that means we'll want to look up the snippet also, so let's make that happen.

When we created the refactoring model, we gave it a snippet_id. Now we'll open app/models/refactoring.rb and add some associations, including for the snippet:

   1  class Refactoring < ActiveRecord::Base
   2    belongs_to :snippet
   3    belongs_to :user
   4  
   5    validates :snippet_id, :presence => true
   6  end

And we should modify Snippet as well:

   1  class Snippet < ActiveRecord::Base
   2    has_many :refactors
   3    belongs_to :user
   4  
   5    validates :user_id, :presence => true
   6  end

By making refactoring belong to snippet, we can say refactoring.snippet to get the snippet for a particular refactoring. And then snippet has many refactorings, so we can say snippet.refactorings to get a list of the refactorings of that snippet. And just as we pair up "refactoring belongs_to snippet" with "snippet has_many refactorings", we should open up the user model (remember it from last time?)... And we'll add some validations while we're in there, just so the user doesn't type utter nonsense for the email address or the username.

(Note: this tutorial originally used old-style "validates_presence_of"-type validators. Now we're using the new style with "validates :field, :presence => true", et cetera. If you see the old ones in the Git Repo, that's why)

   1  class User < ActiveRecord::Base
   2    has_many :snippets
   3    has_many :refactors
   4  
   5    # Include default devise modules. Others available are:
   6    # :token_authenticatable
   7    devise :database_authenticatable, :registerable,
   8           :recoverable, :rememberable, :trackable, :validatable,
   9           :confirmable, :lockable, :timeoutable
  10  
  11    # Setup accessible (or protected) attributes for your model
  12    attr_accessible :email, :password, :password_confirmation, :username
  13  
  14    validates :email, :uniqueness => true, :format => /\A[^@\s]+@[^@\s]+\Z/
  15    validates :username, :uniqueness => true, :format => /^([a-z0-9_-]|\s)+$/i,
  16      :length => 2..20
  17  end

Good stuff. But we still can't just load those views. We need to change them to use the correct routes. Let's start with app/views/refactorings/index.html.erb.

   1      <td><%= link_to 'Show', refactoring %></td>
   2      <td><%= link_to 'Edit', edit_refactoring_path(refactoring) %></td>
   3      <td><%= link_to 'Destroy', refactoring, :confirm => 'Are you sure?', :method => :delete %></td>
   4    </tr>
   5  <% end %>
   6  </table>
   7  
   8  <br />
   9  
  10  <%= link_to 'New Refactoring', new_refactoring_path %>

In the parts above, from the end of index.html.erb, you'll need to change just using the refactoring to providing a good link for it. You'll also need to change the refactoring paths to snippet_refactoring paths. After doing that, you'll get the following changes:

   1      <td><%= link_to 'Show', snippet_refactoring_path(@snippet, refactoring) %></td>
   2      <td><%= link_to 'Edit', edit_snippet_refactoring_path(@snippet, refactoring) %></td>
   3      <td><%= link_to 'Destroy', snippet_refactoring_path(@snippet, refactoring), :confirm => 'Are you sure?', :method => :delete %></td>
   4    </tr>
   5  <% end %>
   6  </table>
   7  
   8  <br />
   9  
  10  <%= link_to 'New Refactoring', new_snippet_refactoring_path(@snippet) %>

That works, right? No? You got an error that looks suspiciously like we never set @snippet? Oh. That's no good. I guess we didn't. So open up the refactorings_controller, and we'll fix that.

Go ahead and add the line "before_filter :capture_snippet" up at the top. Now we just need to write that function:

   1    def capture_snippet
   2      @snippet = Snippet.find(params[:snippet_id]) if params[:snippet_id]
   3    end

Now hit reload in your browser and hey presto, you've got a nice little listing of (no) refactorings for this snippet. You'll need to fix the views for the other actions in the same way, but you're well on your way.

In the refactorings controller, have a look at the "new" action. That's going to need to change. Specifically, change the line "@refactoring = Refactoring.new" into "@refactoring = @snippet ? @snippet.refactorings.build : Refactoring.new". That will attach your new refactoring to the snippet you gave in the URL, assuming you did.

The remaining non-obvious thing you'll need to do is in the form partial for refactorings (app/views/refactorings/_form.html.erb). Notice the line that says "form_for(@refactoring)"? That's not going to work with a nested resource. We need to change that to "form_for([:snippet, @refactoring])" for Rails to work its magic. So try that.

You changed all the paths in new.html and show.html to use the snippet_refactoring stuff, yeah? You can get rid of those links or fix them, but you have to do something if you don't want them to cause errors. You can see how I patched them up in the GitHub repository if you need help.

blog comments powered by Disqus