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:
- The log and public directories must be writable to the web server (
chmod -R 775 log
andchmod -R 775 public
).- 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.- 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:
- 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!- Edit config/database.yml with your database settings.
- Create controllers and models using the generator in
script/generate
Help: Run the generator with no arguments for documentation- See all the tests run by running
rake
.- Develop your Rails application!
- Setup Apache with FastCGI (and Ruby bindings), if you need better performance
- 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:
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.
November 28th, 2005 at 9:11 AM
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…
November 28th, 2005 at 2:17 PM
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.
December 6th, 2005 at 6:51 AM
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;
February 3rd, 2006 at 11:11 AM
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.
February 20th, 2006 at 10:41 PM
Turning off sessions is pretty easy:
class MyController
May 17th, 2006 at 5:36 PM
How easy is it to consume and produce Web Services using Ruby on Rails?
December 30th, 2008 at 10:05 AM
[…] 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 […]