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?
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?
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.
And now we've created it...
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?
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
