Birds on Rails, Part 1

I’ve been working on reimplementing a web application for tracking bird reports in something modern. The current app is held together with Interbase, Delphi, payware e-mail libraries, and variety of handgrown scripts. There aren’t even any input forms that talk directly to the database. Instead you have to e-mail your reports in. The site is uploaded to the server after being statically generated from the database every five minutes or so. This was state of the art in 1995, I suppose. It’s a little questionable today, and the current maintainer/inventor wants to give it up. One of the problems with these sorts of applications is that they’re unmanageable by anybody other than the person who wrote them. Thus it seemed like time to reimplement all this in a slightly more standard manner.

I began by setting up the database tables in MySQL. The lack of foreign key constraints feels a little wrong since I have lots of detail tables in this app. However, it does make the initial data entry a little easier than it might otherwise be.

I tried writing some PHP pages, but that was more difficult than I expected so I thought I’d take a day and see if Ruby on Rails was all it was cracked up to be. I’d looked at it initially, but decided against it because IBiblio doesn’t yet support Ruby. But I’m prototyping this on my desktop Mac, and if Ruby makes my job a lot easier, I could find a different host.

First step was installing RubyGems. (Ruby is either installed by default on Mac OS X, or I had installed it previously. I’m not sure which.) That required sudo but was otherwise uneventful.

$sudo  ruby setup.rb

Next I used gem to install rails:

$ gem install rails --remote

This needs some dependencies installed (rake) and it looks like that requires root access too. OK. Let’s redo that, but with sudo this time:

$ sudo gem install rails --remote

There seem to be a half dozen required dependencies. gem should probably just install them, but in any case I typed “Y” for each one:

~$ sudo gem install rails --remote
Attempting remote installation of 'rails'
Updating Gem source index for: http://gems.rubyforge.org
Install required dependency rake? [Yn]  Y
Install required dependency activesupport? [Yn]  Y
Install required dependency activerecord? [Yn]  Y
Install required dependency actionpack? [Yn]  Y
Install required dependency actionmailer? [Yn]  Y
nstall required dependency actionwebservice? [Yn]  Y
Successfully installed rails-0.14.3
Successfully installed rake-0.6.2
Successfully installed activesupport-1.2.3
Successfully installed activerecord-1.13.0
Successfully installed actionpack-1.11.0
Successfully installed actionmailer-1.1.3
Successfully installed actionwebservice-0.9.3
Installing RDoc documentation for rake-0.6.2...
Installing RDoc documentation for activesupport-1.2.3...
Installing RDoc documentation for activerecord-1.13.0...

lib/active_record.rb:73:64: Skipping require of dynamic string: "active_record/connection_adapters/#{adapter}_adapter"
Installing RDoc documentation for actionpack-1.11.0...

lib/action_controller/assertions.rb:4:69: Skipping require of dynamic string: "#{File.dirname(__FILE__)}/vendor/html-scanner/html/document"
Installing RDoc documentation for actionmailer-1.1.3...
Installing RDoc documentation for actionwebservice-0.9.3...

Fast this isn’t, but hopefully I only have to do this once. Ok. That’s Done. Onto Rails.

I begin by creating the web site:

~/web sites$ rails bbr
      create  
      create  app/controllers
      create  app/helpers
      create  app/models
      create  app/views/layouts
      create  config/environments
      create  components
      create  db
      create  doc
      create  lib
      create  lib/tasks
      create  log
      create  public/images
      create  public/javascripts
      create  public/stylesheets
      create  script/performance
      create  script/process
      create  test/fixtures
      create  test/functional
      create  test/mocks/development
      create  test/mocks/test
      create  test/unit
      create  vendor
      create  vendor/plugins
      create  Rakefile
      create  README
      create  app/controllers/application.rb
      create  app/helpers/application_helper.rb
      create  test/test_helper.rb
      create  config/database.yml
      create  config/routes.rb
      create  public/.htaccess
      create  config/boot.rb
      create  config/environment.rb
      create  config/environments/production.rb
      create  config/environments/development.rb
      create  config/environments/test.rb
      create  script/about
      create  script/breakpointer
      create  script/console
      create  script/destroy
      create  script/generate
      create  script/performance/benchmarker
      create  script/performance/profiler
      create  script/process/reaper
      create  script/process/spawner
      create  script/process/spinner
      create  script/runner
      create  script/server
      create  script/plugin
      create  public/dispatch.rb
      create  public/dispatch.cgi
      create  public/dispatch.fcgi
      create  public/404.html
      create  public/500.html
      create  public/index.html
      create  public/favicon.ico
      create  public/robots.txt
      create  public/javascripts/prototype.js
      create  public/javascripts/effects.js
      create  public/javascripts/dragdrop.js
      create  public/javascripts/controls.js
      create  doc/README_FOR_APP
      create  log/server.log
      create  log/production.log
      create  log/development.log
      create  log/test.log
~/web sites$ 

That was fast. Now let’s run the server:

~/web sites$ ruby scriptserver
ruby: No such file or directory — scriptserver (LoadError)

Hmm. That’s weird. Am I following Windows instructions? Yes, and I’m in the wrong directory to boot. (That backslash should have been a red flag.) One more time:

~/web sites$ cd bbr    
~/web sites/bbr$ ruby script/server
=> Booting WEBrick...
=> Rails application started on http://0.0.0.0:3000
=> Ctrl-C to shutdown server; call with --help for options
[2005-11-26 07:02:54] INFO  WEBrick 1.3.1
[2005-11-26 07:02:54] INFO  ruby 1.8.2 (2004-12-25) [powerpc-darwin8.0]
[2005-11-26 07:02:55] INFO  WEBrick::HTTPServer#start: pid=3036 port=3000

Now let’s see if the server is up by browsing to http://localhost:3000/

Yep. It’s working. Here’s the first page:

Congratulations, you’ve put Ruby on Rails!

Before you move on, verify that the following conditions have been met:

  1. The log and public directories must be writable to the web server (chmod -R 775 log and chmod -R 775 public).
  2. The shebang line in the public/dispatch* files must reference your Ruby installation.
    You might need to change it to #!/usr/bin/env ruby or point directly at the installation.
  3. Rails on Apache needs to have the cgi handler and mod_rewrite enabled.
    Somewhere in your httpd.conf, you should have:
    AddHandler cgi-script .cgi
    LoadModule rewrite_module libexec/httpd/mod_rewrite.so
    AddModule mod_rewrite.c

Take the following steps to get started:

  1. Create empty development and test databases for your application.
    Recommendation: Use *_development and *_test names, such as basecamp_development and basecamp_test
    Warning: Don’t point your test database at your development database, it’ll destroy the latter on test runs!

  2. Edit config/database.yml with your database settings.
  3. Create controllers and models using the generator in script/generate
    Help: Run the generator with no arguments for documentation
  4. See all the tests run by running rake.
  5. Develop your Rails application!
  6. Setup Apache with FastCGI (and Ruby bindings), if you need better performance
  7. Remove the dispatches you don’t use (so if you’re on FastCGI, delete/move dispatch.rb, dispatch.cgi and gateway.cgi)

Trying to setup a default page for Rails using Routes? You’ll have to delete this file (public/index.html) to get under way. Then define a new route in config/routes.rb of the form:

  map.connect '', :controller => 'wiki/page', :action => 'show', :title => 'Welcome'

Having problems getting up and running? First try debugging it yourself by looking at the log files.
Then try the friendly Rails community on the web or on IRC
(FreeNode#rubyonrails).

Seems like there’s a lot of work to do. Let’s get started.

Step 1. Change the permissions. Done. You would think the installer could have set the permissions properly on the directories it itself created.

Step 2. Set the shebang line in the three dispatch files. Those are set to #!/usr/bin/ruby. That seems to work. At least I have a /usr/bin/ruby. (Again, the installer should have been able to figure this out automatically, and adjust it if necessary.)

Step 3. I’m not running this on Apache just yet, but let’s check anyway:

~/web sites/bbr/public$ grep cgi /private/etc/httpd/httpd.conf
LoadModule cgi_module         libexec/httpd/mod_cgi.so
AddModule mod_cgi.c
    ScriptAlias /cgi-bin/ "/Library/WebServer/CGI-Executables/"
    #AddHandler cgi-script .cgi
# Format: Action media/type /cgi-script/location
# Format: Action handler-name /cgi-script/location
#ErrorDocument 404 /cgi-bin/missing_handler.pl
# support/phf_abuse_log.cgi.
#<Location /cgi-bin/phf*>
#    ErrorDocument 403 http://phf.apache.org/phf_abuse_log.cgi
~/web sites/bbr/public$ grep rewrite /private/etc/httpd/httpd.conf
LoadModule rewrite_module     libexec/httpd/mod_rewrite.so
AddModule mod_rewrite.c
<ifmodule mod_rewrite.c>

Rewriting is turned on. .cgi handling isn’t. I’ll need to fix that if I’m trying to integrate this with Apache, but for now I can skip that step.

Now to connect to my databases. First, Rails wants me to create separate production, test, and development databases. That seems like a good idea regardless, so let’s do that. The production database is named bbr:

$ mysqldump -u root -p  bbr > bbr.sql
~/backups$ mysql -u root -p
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or g.
Your MySQL connection id is 117 to server version: 5.0.16-standard

Type 'help;' or 'h' for help. Type 'c' to clear the buffer.

mysql> create database bbr_test;
Query OK, 1 row affected (0.01 sec)

mysql> use bbr_test;
Database changed
mysql> source bbr.sql;
...
mysql> create database bbr_development;
Query OK, 1 row affected (0.01 sec)

mysql> use bbr_development;
Database changed
mysql> source bbr.sql;

Query OK, 0 rows affected (0.00 sec)
...
mysql> quit
Bye

Now I have to setup database.yml. Looking inside it figured out most things automatically. (I wonder how it knew which database to use? It probably guessed from the name of the directory I’d created to hold the site.)

development:
  adapter: mysql
  database: bbr_development
  username: root
  password:
  socket: /tmp/mysql.sock

  # Connect on a TCP socket.  If omitted, the adapter will connect on the
  # domain socket given by socket instead.
  #host: localhost
  #port: 3306
  
# 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: mysql
  database: bbr_test
  username: root
  password:
  socket: /tmp/mysql.sock

production:
  adapter: mysql
  database: bbr_production
  username: root
  password: 
  socket: /tmp/mysql.sock

I just need to change the name of the production database to bbr, add the passwords, and delete the lines for the databases I’m not using.

Time to start writing pages. Let’s put the main page at /bbr (One thing I like a lot about Rails is that it let’s you organize according to a natural URL structure. My PHP apps always have much too large query strings. WordPress works around that, but it requires a lot of mod_rewrite Voodoo.) First let’s navigate to /bbr (even though the page doesn’t exist yet) and Whammo! I just found the first major flaw in Rails:

Rails setting session ID

It’s trying to set a cookie for no reason whatsoever, even for a page that doesn’t exist! What’s worse, it’s a session ID! This is completely contrary to the web architecture, and a very common mistake made by developers who are mired in 1980s style of applications and have never understood the Web, and probably never will. My PHP version and the existing site I’m replacing are completely cookie-free. This is not a good sign, and does not leave me with a warm-and-fuzzy feeling; but let’s deny the setting of the cookie, proceed onward, and see what happens.

We get a routing error as expected because there’s no such page. Let’s go ahead and generate that page:

~/web sites/bbr$ script/generate controller bbr   
      exists  app/controllers/
      exists  app/helpers/
      create  app/views/bbr
      exists  test/functional/
      create  app/controllers/bbr_controller.rb
      create  test/functional/bbr_controller_test.rb
      create  app/helpers/bbr_helper.rb

Now the page shows

Unknown action

No action responded to index

So I have to edit the controller script for bbr:

$ bbedit app/controllers/bbr_controller.rb

It initially looks like this:

class BbrController < ApplicationController
end

Now I define an index method like so:

class BbrController < ApplicationController

  def index
    render_text "Hello Birds!"
  end 

end

Save it, reload the page, and we see the text “Hello Birds”. Cool. Next let’s fill it with HTML instead. Trying the simplest thing that could possibly work, I pasted a bunch of HTML into the index method. However, that produced a syntax error. Hmm, there has to be a simpler way to do this than printing all the strings?

Or maybe not. Ruby lets me put line breaks in string literals, so maybe I just have to paste it into the string literals. How do I escape double quotes in string literals in Ruby? As usual, Google comes to the rescue, and tells me to use a backslash. OK. That worked. Ugly but functional. there’s probably a smarter way to do this I just don’t know yet, but that will suffice for now.

Now let’s see if we can pull some data out of the database and put it in the page. The Rails examples I’ve seen have all been quite flat databases. Mine’s more complex; but let’s start with something flat: making a list of all the sites in the sites table and putting it in an unordered list. First I generate the model and the controller:

~/web sites/bbr$ script/generate model Site
      exists  app/models/
      exists  test/unit/
      exists  test/fixtures/
      create  app/models/site.rb
      create  test/unit/site_test.rb
      create  test/fixtures/sites.yml
~/web sites/bbr$ script/generate controller Site  
      exists  app/controllers/
      exists  app/helpers/
      create  app/views/site
      exists  test/functional/
      create  app/controllers/site_controller.rb
      create  test/functional/site_controller_test.rb
      create  app/helpers/site_helper.rb

Since the model is named Site, Rails looks for a table named “sites”. Allegedly, it understands English plurals and the difference between upper and lower case.

At this point I seem to be going off script. I just want to get a list of the sites in the database, and Rails wants me to create a new page with the ability to insert new sites. This is a common criticisim of Rails. It works great if you do what it wants you to, and not so well if you deviate from the approved path.

Let’s edit the controller:

class SiteController < ApplicationController
end

to

class SiteController < ApplicationController
  scaffold :site
end

When I try to connect to http://127.0.0.1:3000/site/list it refuses user@localhost with no password. I wonder if I need to restart the web server to get it to pick up the config changes that included the username and password? Nope, that didn’t do it. Or a problem with old style passwords? Doesn’t seem likely. I’m using the latest version of MySQL 5. Maybe it’s this:

sudo gem install mysql -- --with-mysql-dir=/usr/local/mysql

I skipped this step the first time through because I thought it wsas sintalling mysql which I already have, but apparently gem only installs the Ruby bindings for MySQL. Nope. Restarted the web server. Same problem. Let’s try

sudo gem install mysql — –with-mysql-dir=/usr/local/mysql-standard-5.0.16-osx10.4-powerpc-64bit

instead. Nope same problem. Wait, I just noticed something. The error message is

Access denied for user ''@'localhost' (using password: NO)

It’s not user@localhost, it’s the user empty string at localhost. Perhaps a clue?

Ah. OK. It looks like gem did not properly install the MySQL connector. Let’s see if we can fix that. Hmm, after much futzing this may compile it:

/usr/lib/ruby/gems/1.8/gems/mysql-2.7$ sudo ruby extconf.rb -- --with-mysql-config
Password:
checking for mysql_ssl_set()... no
checking for mysql.h... yes
creating Makefile
/usr/lib/ruby/gems/1.8/gems/mysql-2.7$ make
gcc -fno-common -arch i386 -arch ppc -g -Os -pipe -fno-common -arch i386 -arch ppc -pipe -pipe -fno-common  -I. -I/usr/lib/ruby/1.8/powerpc-darwin8.0 -I/usr/lib/ruby/1.8/powerpc-darwin8.0 -I. -DHAVE_MYSQL_H  -I/usr/local/mysql/include -Os -arch ppc64 -fno-common -c mysql.c
gcc: cannot read specs file for arch `i386'
make: *** [mysql.o] Error 1
/usr/lib/ruby/gems/1.8/gems/mysql-2.7$ h | grep gcc
  512  sudo gcc_select 3.3
  518  gcc -version
  519  gcc --version
  540  gcc -version
  541  gcc --version
  556  h | grep gcc
/usr/lib/ruby/gems/1.8/gems/mysql-2.7$ sudo gcc_select 4.0
Default compiler has been set to:
gcc version 4.0.0 (Apple Computer, Inc. build 5026)
/usr/lib/ruby/gems/1.8/gems/mysql-2.7$ sudo make
gcc -fno-common -arch i386 -arch ppc -g -Os -pipe -fno-common -arch i386 -arch ppc -pipe -pipe -fno-common  -I. -I/usr/lib/ruby/1.8/powerpc-darwin8.0 -I/usr/lib/ruby/1.8/powerpc-darwin8.0 -I. -DHAVE_MYSQL_H  -I/usr/local/mysql/include -Os -arch ppc64 -fno-common -c mysql.c
cc  -dynamic -bundle -undefined suppress -flat_namespace  -L"/usr/lib" -o mysql.bundle mysql.o  -arch ppc64 -L/usr/local/mysql/lib -lmysqlclient -lz -lm  -lpthread -ldl -lobjc  
ld64 warning: in /usr/lib/libz.dylib, file does not contain requested architecture
ld64 warning: in /usr/lib/libobjc.dylib, file does not contain requested architecture

Hmm, maybe the problem is that bit about -arch i386? or the duplicated flags? Let me try to edit that manually. Hmm. Same warning. However after installing it there’s finally some effect. The error message in Ruby has changed to “uninitialized constant Mysql”.

Maybe it’s because I’m using the 64-bit version of MySQL? Switching to the 32-bit version does seem to make a difference. I can now run the mysql-ruby tests and avoid link errors. The tests fail but I can run them.

~/downloads/mysql-ruby-2.7.1$ ruby ./test.rb localhost root
Loaded suite ./test
Started
.........................................................FFF......F.................F.........................
Finished in 0.470085 seconds.

  1) Failure:
test_fetch_bigint(TC_MysqlStmt2) [./test.rb:799]:
<[-1]> expected but was
<[9223372036854775807]>.

  2) Failure:
test_fetch_bigint_unsigned(TC_MysqlStmt2) [./test.rb:812]:
<[-1]> expected but was
<[0]>.

  3) Failure:
test_fetch_binary(TC_MysqlStmt2) [./test.rb:1009]:
<["abc"]> expected but was
<["abc�00�00�00�00�00�00�00"]>.

  4) Failure:
test_fetch_double(TC_MysqlStmt2) [./test.rb:860]:
<-1.79769313486232e+308> expected but was
<-1.79769313486232e+308>.

  5) Failure:
test_fetch_timestamp(TC_MysqlStmt2) [./test.rb:950]:
<[#<Mysql::Time:2037-12-31 23:59:59>]> expected but was
<[#<Mysql::Time:0000-00-00 00:00:00>]>.

110 tests, 344 assertions, 5 failures, 0 errors

Finally making progress. Now I get a different error message:

 Mysql::Error in Site#list

Access denied for user 'root'@'localhost' (using password: YES)

But at least this time it recognizes the user as root. Hmm, maybe the problem is that the new 32-bit database doesn’t yet have a password; and that seems to be it. Finally! Now it’s working, and I can move forward.

I’m not sure exactly where the problem lies. However, the error messages could certainly have been more helpful. Tomorrow I’ll try to get back to actually building the application.

7 Responses to “Birds on Rails, Part 1”

  1. Agylen Says:

    Another migration to Rails

    After Graham Glass, here comes another old Java hand diving into Ruby on Rails.
    Elliotte Rusty Harold: “I tried writing some PHP pages, but that was more difficult than I expected so I thought I’d take a day and see if Ruby on Rails was a…

  2. George Bailey Says:

    Well I’m glad you’re going through this so I don’t have to. I have a project planned, and I found a host that supports RoR, but I’ve been too busy with actual work to do anything further. asmallorange.com is inexpensive and seems to be pretty good.

  3. ALan Says:

    Just a quickie, you *can* have foreign keys in later version of MySQL if you use the InnoDB engine.

    CREATE TABLE users (
    id int(11) not null auto_increment,
    login varchar(80) default null,
    primary key (id),
    index login_index (login)
    ) type=InnoDB;

    create table projects (
    id int(11) not null auto_increment,
    name varchar(128) not null,
    owner_id int(11) not null,
    primary key(id),
    constraint fk_project_owners foreign key (owner_id) references users(id)
    ) type=InnoDB;

  4. Erkki Says:

    You can turn off session support in your controllers (for the whole application, or for some controllers or even only some actions). That way you will not get the default session cookie.

    See http://wiki.rubyonrails.org/rails/pages/HowtoPerActionSessionOptions for more information.

  5. Anon Says:

    Turning off sessions is pretty easy:

    class MyController

  6. Bala Says:

    How easy is it to consume and produce Web Services using Ruby on Rails?

  7. Mokka mit Schlag » 2008 New Year’s Resolution Says:

    […] I toyed with a year or two ago: I’d like to reimplement the now defunct NYC Bird Report on top of a portable, maintainable […]