stevengharms.com

Sententiae viri ex temporibus duobus

Getting Back on the Rails: Headaches Around Rails 2.x

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

  1. Make a working directory ~/experiment.
  2. Make an experimental rails install ~/experiment/exp with rails -d sqlite3 exp
  3. 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 http://localhost:3000 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

rake db:migrate

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

Hypothesize

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 ;)

  1. Re-create the exp and control rails installs
  2. Get a list of all the files: find exp -f > rawfiles
  3. Use sed to remove the leading exp/ from rawfiles
  4. 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'
  1. 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.

  2. diff the output files:

    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

    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

  3. 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.

  4. 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”.

Output below:

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

Analyze differences

The files with a notable difference are these:

  1. /app/views/posts/edit.html.erb
  2. /app/views/posts/index.html.erb
  3. /app/views/posts/new.html.erb
  4. /app/views/posts/show.html.erb
  5. /db/migrate/001_create_posts.rb
  6. /test/fixtures/posts.yml

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.

  1. Modify the experiment’s migration file ( add t.string :title, t.datestamp… etc. )
  2. rake db:migrate both
  3. Transplant the control’s template files into the experimental’s template directory
  4. script/server the experimental
  5. 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.

  1. 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.

  2. 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.

script/generate –help

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.

I’ll confirm that with rails-list. I just wanted to save my research here for the benefit of others.

Comments