I decided to spend some of my Christmas break taking a look at Ruby on Rails again and trying to get an application together under this application stack. Since I last looked at Rails, it had crossed the 1.x to 2.x version marker and several changes manifested. For someone trying to follow the 1.x tutorials or the canonical reference book Agile Web Development With Rails, one doesn’t make very much progress before the changes with respect to scaffolding bite one. One may need a bit of a tutorial on Rails 2.x scaffolding before being “pretty close” to training documentation. An excellent tutorial for this content was found at Fairleads.
Yet as I followed this through, I found myself stymied by the same error reported by “James”
Thanks now I easily understand how scaffolding changed in 2.0.
But I still have a problem when I followed the tutorial everything went fine but i am not presented with any sort of fields to enter information.
And when i add stuff to the database by hand and I list from within the scaffold I am not getting the information that is entered into the database I am just getting a blank entry.
At first I thought my problem was that I was trying to use a PostgreSQL back end, or perhaps it was because I was using MySQL, etc. But nevertheless I still ran into the same problem no matter which db back-end I used.
I then found another tutorial at SapphireSteel. Curiously, this worked ( the db back end was sqlite3 ). This tutorial is meant to show off the Sapphire Steel product, but Huw, the author, has added, graciously, a “here’s what Rails is doing on the back end” translation for each step that his product automates.
With these data points my working hypothesis was this:
- Use SQLite3
- Follow Huw’s instructions, which use a more specialized invocation of “script/generate scaffold” ( control )
- Duplicate these instructions using a more primitive invocation of “script-generate scaffold” that’s more familiar ( experiment )
Step 1: Set up the working directory
- Make a working directory ~/experiment.
- Make an experimental rails install ~/experiment/exp with rails -d sqlite3 exp
- Make an control rails install ~/experiment/control with rails -d sqlite3 control
Step 2: Check the config/database.yml
# SQLite version 3.x # gem install sqlite3-ruby (not necessary on OS X Leopard) development: adapter: sqlite3 database: db/development.sqlite3 timeout: 5000 # Warning: The database defined as 'test' will be erased and # re-generated from your development database when you run 'rake'. # Do not set this db to the same as development or production. test: adapter: sqlite3 database: db/test.sqlite3 timeout: 5000 production: adapter: sqlite3 database: db/production.sqlite3 timeout: 5000
Step 3: Verify that they are the same between the two installs:
bash-3.2$ diff control/config/database.yml exp/config/database.yml
Step 4: In the control section issue:
script/generate scaffold post title:string body:text created_at:datetime
This follows the SapphireSteel documentation. In the experiment do not implement the fully fleshed out line:
script/generate scaffold post
Each of these steps generates a lot of output as various files are conjured.
I also tried running script/server for each of these and then visiting site to verify that the Rails install, at its most basic was running. This was valid.
Step 5: Examine the diff between the control’s db/migrate/001_create_posts.rb
bash-3.2$ diff -y control/db/migrate/001_create_posts.rb exp/db/migrate/001_create_posts.rb
Note: Changed format to be vertical so that you don’t have horizontal bleed in web format
class CreatePosts < ActiveRecord::Migration def self.up create_table :posts do |t| t.string :title t.text :body t.datetime :created_at t.timestamps end end def self.down drop_table :posts end end class CreatePosts < ActiveRecord::Migration def self.up create_table :posts do |t| > > > t.timestamps end end def self.down drop_table :posts end end
Step 6: Difference between the two migrations are these three lines.
t.string :title t.text :body t.datetime :created_at
Step 7: Test migrations
in both the experiment/exp and experiment/control directories. Roll back the changes with:
rake db:migrate VERSION=0
Step 7: Make the experimental 001… migration look identical to the control-generated experimental 001… migration
I actually typed the lines in, but you could use a tool like vimdiff, or, more swiftly, copy the control file into the experimental file like so:
cp control/db/migrate/001_create_posts.rb exp/db/migrate/001_create_posts.rb
You could verify their parity by using diff.
Step 8: Migrate both with rake db:migrate
At this point they are using the same migration, the same db back end, etc. They should be the same in function.
Start up script/server on the control
It is rich functioning, when adding “new” entries we have all the basic scaffolded elements and things work as expected.
Stop control, start script/server on experiment.
Breaks! When you hit “new”, you are not given the fields to fill in! Instead you are taken directly back to the index! This is the broken behavior described at fairleads.
Step 9: Try to fix it ;)
- Re-create the exp and control rails installs
- Get a list of all the files: find exp -f > rawfiles
- Use sed to remove the leading exp/ from rawfiles
for i in ``cat rawfiles\; do diff exp/$i control/$i; done`
I removed the hexidecimal noise from the diff.
bash-3.2$ for i in `cat rawfiles `; do diff exp/$i control/$i; done 9c9 < protect_from_forgery # :secret => 'hex stuff' --- > protect_from_forgery # :secret => 'hex stuff' 40,41c40,41 < :session_key => '_exp_session', < :secret => 'hex stuff' --- > :session_key => '_control_session', > :secret => 'hex stuff'
OK, so they’re the same install. Now. Let’s do the scaffold command the two different ways and record the output sent to STDOUT to text files.
diff the output files:
bash-3.2$ diff -y control.log ../exp/experiment.log exists app/models/
route map.resources :posts
bash-3.2$ diff -y control.log ../exp/experiment.log exists app/models/ exists app/controllers/ exists app/helpers/ create app/views/posts exists app/views/layouts/ exists test/functional/ exists test/unit/ create app/views/posts/index.html.erb create app/views/posts/show.html.erb create app/views/posts/new.html.erb create app/views/posts/edit.html.erb create app/views/layouts/posts.html.erb create public/stylesheets/scaffold.css dependency model exists app/models/ exists test/unit/ exists test/fixtures/ create app/models/post.rb create test/unit/post_test.rb create test/fixtures/posts.yml create db/migrate create db/migrate/001_create_posts.rb create app/controllers/posts_controller.rb create test/functional/posts_controller_test.rb create app/helpers/posts_helper.rb route map.resources :posts
OK, so they both generated the same files. In theory, if I use the find technique above to find all the files in either the exp directory or the control directory I can get a listing of all the files that both directories share. Using the foreach diff technique described above, I should be able to find out exactly what’s different between the two.
Rehash: Build file list, find diffs, remove the session key difference data we saw above, that’s OK to be different. Keep in mind, below, when you see a “>” character, it means “this is something the control has that the experimental does not”.
Diffing: [exp/app/views/posts/edit.html.erb] to [control/app/views/posts/edit.html.e 6a7,21 > <b>Title</b><br /> > <%= f.text_field :title %> > </p> > > <p> > <b>Body</b><br /> > <%= f.text_area :body %> > </p> > > <p> > <b>Created at</b><br /> > <%= f.datetime_select :created_at %> > </p> > > <p> Diffing: [exp/app/views/posts/index.html.erb] to [control/app/views/posts/index.html 4a5,7 > <th>Title</th> > <th>Body</th> > <th>Created at</th> 8a12,14 > <td><%=h post.title %></td> > <td><%=h post.body %></td> > <td><%=h post.created_at %></td> Diffing: [exp/app/views/posts/new.html.erb] to [control/app/views/posts/new.html.erb 6a7,21 > <b>Title</b><br /> > <%= f.text_field :title %> > </p> > > <p> > <b>Body</b><br /> > <%= f.text_area :body %> > </p> > > <p> > <b>Created at</b><br /> > <%= f.datetime_select :created_at %> > </p> > > <p> Diffing: [exp/app/views/posts/show.html.erb] to [control/app/views/posts/show.html.e 0a1,15 > <p> > <b>Title:</b> > <%=h @post.title %> > </p> > > <p> > <b>Body:</b> > <%=h @post.body %> > </p> > > <p> > <b>Created at:</b> > <%=h @post.created_at %> > </p> > Diffing: [exp/db/migrate/001_create_posts.rb] to [control/db/migrate/001_create_post 3a4,6 > t.string :title > t.text :body > t.datetime :created_at Diffing: [exp/test/fixtures/posts.yml] to [control/test/fixtures/posts.yml] 3,7c3,11 < # one: < # column: value < # < # two: < # column: value --- > one: > title: MyString > body: MyText > created_at: 2007-12-30 19:22:51 > > two: > title: MyString > body: MyText > created_at: 2007-12-30 19:22:51
The files with a notable difference are these:
I’m going to take a stab that my problem is in the new.html.erb because, uh, my problems, thus far, have centered around what happens when I hit the new button.
bash-3.2$ cat control/app/views/posts/new.html.erb <h1>New post</h1> <%= error_messages_for :post %> <% form_for(@post) do |f| %> <p> <b>Title</b><br /> <%= f.text_field :title %> </p> <p> <b>Body</b><br /> <%= f.text_area :body %> </p> <p> <b>Created at</b><br /> <%= f.datetime_select :created_at %> </p> <p> <%= f.submit "Create" %> </p> <% end %> <%= link_to 'Back', posts_path %> bash-3.2$ cat exp/app/views/posts/new.html.erb <h1>New post</h1> <%= error_messages_for :post %> <% form_for(@post) do |f| %> <p> <%= f.submit "Create" %> </p> <% end %> <%= link_to 'Back', posts_path %> bash-3.2$
Well, this makes it pretty apparent what the problem would be. They both have the same error_messages_for line, so the difference is the way the form is defined. The control has all the widgets (text_field, text_area) all handily defined. From here we can see what’s causing the problem.
Running rake in the control will create the database entries ( yay! ) and the scaffolding has all the right widgets to take advantage of this database structure. When db:migrate command is passed to the experimental, the database structure changes, but when you try to interface with the scaffold, well, they don’t take advantage of the model because the template is pretty brain-dead.
To get things working rightly, we can test things manually.
- Modify the experiment’s migration file ( add t.string :title, t.datestamp… etc. )
- rake db:migrate both
- Transplant the control’s template files into the experimental’s template directory
- script/server the experimental
- It should work like expected.
And it does.
Step 10: Conclusions
Now this really messes with my normal use case with rails. I would come up with a model ( Movies, Posts, Cards, etc. ) and then I would create the migration. Back and forth I would go, creating new columns as needed. Due to the dynamic scaffolding, my views would adjust as necessary, now my views are stuck with my first guess on how to scaffold the data, and now they need to be updated.
Option: Once the “proper” columns are figured out, re-run the scaffold generation command “knowing what I wish I had known then”. That is, while your first execution was “script/generate scaffold post” you ultimately get to writing “script/generate scaffold post title:string foo:string bar:text”, etc.
Find a way to make rails re-sync the view components with the model ( Rails-y-er, I should think ).
So, to the documentation for script/generate I go.
Points me to script/generate scaffold –help, the textual part of which is printed below:
Description: Scaffolds an entire resource, from model and migration to controller and views, along with a full test suite. The resource is ready to use as a starting point for your restful, resource-oriented application. Pass the name of the model, either CamelCased or under_scored, as the first argument, and an optional list of attribute pairs. Attribute pairs are column_name:sql_type arguments specifying the model's attributes. Timestamps are added by default, so you don't have to specify them by hand as 'created_at:datetime updated_at:datetime'. You don't have to think up every attribute up front, but it helps to sketch out a few so you can start working with the resource immediately. For example, `scaffold post title:string body:text published:boolean` gives you a model with those three attributes, a controller that handles the create/show/update/destroy, forms to create and edit your posts, and an index that lists them all, as well as a map.resources :posts declaration in config/routes.rb. Examples: `./script/generate scaffold post` # no attributes, view will be anemic `./script/generate scaffold post title:string body:text published:boolean` `./script/generate scaffold purchase order_id:integer amount:decimal`
Based on this, I think that the recommended way of handling this problem is to use a “more informed” script/generate command, but this seems to be clunky and rather un-agile.