College Park Crime Reports

Basic Setup: A Quick Rails Recap

The following section is intended to be pretty familiar to experienced Rails users, but I include it as a record of what I did, and hopefully to be helpful to those new to Rails. I have taken a similar, though not identical approach, to the one outlined in Bruce Tate's and Curt Hibbs' book Ruby on Rails: Up and Running. I would recommend this book to anyone who likes to learn by doing and wants to learn about Rails.

Files, Models, and Data

First it's time to create the project's directory structure, the database, users and passwords, and a model of our first class. The first thing I do is create three new MySQL databases: one for development, one for testing, and one for production. This is a Rails standard. I use an online configuration tool at my web host to do this. I also create a new user and password for access to the databases, and I note all this information--DB names, user names, and passwords--for use in Rails configuration.

Now I open a UNIX shell session and change directories into my projects folder.

$ rails cpcr
$ cd cpcr
$ vi config/database.yml
$ chmod 600 config/database.yml
$ ruby script/generate model CrimeReport

The rails command creates directories and files to hold all the information necessary for the web application. I'm going to be most interested in the app, config, db, and test folders. I now change directories into the new cpcr folder.

Next, I create the database.yml file. As per Rails standards, I'm describing three databases, labeled development, test, and production. Here is a representative snippet:

development:
  adapter: mysql
  database: cpcr_development
  username: <some user>
  password: <somepassword>
  host: <usually localhost>
  
test:
... etc ...
This file is in the YAML format. Depending on your mood, YAML stands for "Yet Another Markup Language", or the Stallman-esque "YAML Ain't Markup Language". In these examples, I'm using the vi editor, in part because I'm so hardcore, but also because it's available on almost all UNIX systems and is a great way to make quick edits to files from the shell. It takes a little getting used to though. One could use another text editor and/or FTP to edit files.

The config file I just made is in a web-accessible directory. I immediately need to prevent others from reading it, since it contains MySQL usernames and passwords. So I chmod it so that I can read it (and the Rails process is happily running as my user) but others, including Apache, cannot.

Now it's time to generate the model for our first class, CrimeReport. I run the generate script, passing it the 'model' and 'CrimeReport' arguments to tell it I want a model stub for a CrimeReport class. Like the rails command earlier, this generates directories and files in my project folder.

Now I'm going to instantiate the persistence layer for my application, which is just to say I'm going to specify a table for the CrimeReports class. In the old days, this step marked the beginning of a commitment to keep the database, the application code, and the glue code that handles CRUD actions in synch. Rails doesn't relieve you entirely of this responsibility, but it does a lot of the work of mapping your objects with your DB records with just a little information.

$ vi db/migrate/001__create_crime_reports.rb

When I used the generate script, it created a file in the db/migrate folder that specifies instructions for setting up a table for my new CrimeReports model. The file is just a stub. I go ahead and fill out the up method like so:

class CreateCrimeReports < ActiveRecord::Migration
  def self.up
    create_table :crime_reports do |t|
      t.column 'address', :string                                                  
      t.column 'date', :string # for now                                           
      t.column 'description', :string                                              
      t.column 'case_number', :string
      t.column 'lat', :float
      t.column 'long', :float                                                      
    end
  end
...
end

This looks a lot like DDL, like a Create Table command. It contains the same information, and when you use the rake command (coming right up), the same thing happens, but the information is expressed in a Ruby idiom, and its execution is now brought under the control of the Rails system. The file is in a folder called migrate because migration is Rails approach to managing the database. You can change the definition of your database by altering these migration files. The down method describes how to clean up the existing structure, and the up method describes how to set up the new structure.

In that light, let's look at the data I'm describing. Address, date, description, all seem pretty obvious. I'm adding case_number so that I, and other users, may find other information the police are maintaining under those numbers. The lat and long columns, for latitude and longitude, are here so I can feed them to Google maps. You may have noticed there's no id column to serve as a primary key. Rails handles that for me, by convention.

Hey, dig that do |t|. What is t? It's a reference to the table you just created, and all the t.column business that follows is instructing the caller of the method to add these columns to the new table. This isn't a configuration language, and it certainly isn't XML. It's just Ruby.

I mentioned you needed to use a command to put the migration file into action. Here it is:

$ rake migrate

The rake program is, briefly, a Rails analog to make, or ant from the Java world. It allows the execution of scripts to manage your application. In this case, the 'migrate' argument passed to rake executes the migration scripts. It creates the database table I've described in the up method.

Testing

Rails takes automated testing seriously, which is one reason why so many proponents of agile methodologies are interested in Rails. At this point in my application development effort, I can already run unit tests on my models, in this case, the CrimeReport model, by using rake:

$ rake test:units
1 tests, 1 assertions, 0 failures, 0 error

What did I just test? Well, when I used the generate script, a set of test files were generated for me, in the test folder. Let's take a look at the previously-generated unit test I just ran:

$ vi test/unit/crime_report_test.rb
class CrimeReportTest < Test::Unit::TestCase
...
  # Replace this with your real tests.
  def test_truth
    assert true
  end
...

Again, it's just a stub. By calling assert true, you're testing nothing. I need to put some real code in here. I'll start simple.

def test_crud 
  cr = CrimeReport.new
  assert cr.save 
end

Now I'm testing that I can create a new CrimeReport object and then call a save method on that object. Does the test pass? Yep. Did I ever code up a CrimeReport class? Nope. When I generated the CrimeReport model, Rails did produced a stub class: app/models/crime_report.rb. Here it is.

class CrimeReport < ActiveRecord::Base
end

Obviously, the save method is inherited from the parent class ActiveRecord::Base, but how does the parent class's code know about the structure I have in mind for CrimeReport? It gets it on the fly, from the structure of the database. Rails makes heavy use of Ruby's splendid tools for metaprogramming. It would take too long to go into it here, but because I defined those columns in the migration script and executed that script, Rails can now dynamically construct objects with the right attributes and setter and getter methods.

One of the things I like about testing first is that you start trying to use your classes before you code them. The basic test I've coded creates a new CrimeReport object and saves it to the database. What have I specified about the object? Nothing. Do I like that? Should someone be able to create a persistent record of a crime report without data? No. Some fields should be mandatory. A description, for one. So, actually, my test should look like this.

def test_crud 
  cr          = CrimeReport.new
  assert !cr.save 
end

This fails. I have explicitly introduced the requirement that a crime report without information can not be saved by the system, and now I need to implement it.

I will edit the model class stub. I can express the requirement of a description with the validates_presence_of instruction:

$ vi app/models/crime_report.rb
  class CrimeReport < ActiveRecord::Base
    validates_presence_of :description
  end 

But this leads to more detailed thinking. What else is essential? Well, if I want every report to appear on a Google Map, I need coordinates for every one: a latitude and longitude. I adjust my test to isolate and express these specific requirements.

def test_crud

  cr             = CrimeReport.new
  # Test long requirement
  cr.description = 'test description'
  cr.lat         = 38.1250
  assert !cr.save, "Save should not have been successful without required field 'long'"

  cr             = CrimeReport.new
  # Test lat requirement
  cr.description = 'test description'
  cr.long        = -74.381250
  assert !cr.save, "Save should not have been successful without required field 'lat'"

  cr             = CrimeReport.new
  # Test description requirement
  cr.lat         = 38.1250
  cr.long        = -74.381250
  assert !cr.save, "Save should not have been successful without required field 'description'"

cr             = CrimeReport.new
  # Test minimal requirements
  cr.description = 'test description'
  cr.lat         = 38.1250
  cr.long        = -74.381250
  assert cr.save, "Save should have been successful"
end

Of course, this doesn't work yet, so I will edit the model class accordingly.

class CrimeReport < ActiveRecord::Base
  validates_presence_of :description
  validates_presence_of :long
  validates_presence_of :lat
end
$ rake test:units
1 tests, 4 assertions, 0 failures, 0 errors

Let's test things just a little more thoroughly, to reassure ourselves everything works, by adding this code to the end of the test_crud method:

  assert_not_nil cr2 = CrimeReport.find(cr.id)
  assert_equal cr, cr2

  cr2.description = 'foo description'
  assert cr2.save
	
  cr3	            = CrimeReport.find(cr2.id)
  assert_equal 'foo description', cr3.description
	
  assert cr3.destroy
	
  assert_equal 2, CrimeReport.count
$ rake test:units
1 tests, 10 assertions, 0 failures, 0 errors