Ruby on Rails Project - To Do List # 2

It's been a month since I made the first post for this personal project. What can I say, it was the holidays, and I had other stuff on my mind. I'm still interested in learning Ruby and Rails, so I'm not abandoning the project. Here's part two.

My To-Do List web application is going to start off pretty simple. The first thing I need to do is consider the entities that are needed. To begin I will just have a Project entity and a Task entity. A Project can contain one or more Tasks. A Project will have one attribute, a Name. A Task will have a couple attributes: Name, Due Date, Project ID and an Is Complete flag.

Just like Rails provided us with scripts to generate our directory structure, it also provides us with a script to generate our Model. Remember, Rails is based on the MVC (Model, View, Controller) Design Pattern. The Model creation syntax is

$ ruby script/generate model model_name

Now we create our two Models

$ ruby script/generate model Project
$ ruby script/generate model Task

Here's an example of the output you should receive

exists app/models/
exists test/unit/
exists test/fixtures/
create app/models/project.rb
create test/unit/project_test.rb
create test/fixtures/projects.yml
create db/migrate
create db/migrate/001_create_projects.rb

As you can see, it not only generates our Model, but also the Test files for running our Unit Tests and files for migrating our database changes. This is one thing I enjoy about Rails, you don't have to use a tool to create your database tables and the columns in those tables, you can use the files that are generated. I recommend doing this, since Rails will automatically create Unique ID fields for you.

So, now that we have the Model created, we will add the code to the migrate file to create the columns we need.

If you look at the file 001_create_projects.rb in TextMate, you should see the following

class CreateProjects < ActiveRecord::Migration
  def self.up
    create_table :projects do |t|
    end
  end

  def self.down
    drop_table :projects
  end
end

If we performed a migration with this, it would create a projects table for us, and it would have a Unique ID field, but nothing else. We can modify it to add the fields we need, which is just a Name.

class CreateProjects < ActiveRecord::Migration
  def self.up
    create_table :projects do |t|
      t.column :name, :string
    end
  end

  def self.down
    drop_table :projects
  end
end

The code for 002_create_tasks.rb is

class CreateTasks < ActiveRecord::Migration
  def self.up
    create_table :tasks do |t|
      t.column :name, :string
      t.column :due_date, :datetime
      t.column :project_id, :integer, :null => false
      t.column :is_complete, :boolean
    end
  end

  def self.down
    drop_table :tasks
  end
end

We can now use Rake to apply these changes to the database. You should receive output similar to this

$ rake db:migrate
(in /Users/punkcoder/Development/Ruby/pctask)
== CreateProjects: migrating ==================================
-- create_table(:projects)
  -> 0.0257s
== CreateProjects: migrated (0.0259s) ===========================

== CreateTasks: migrating ==================================
-- create_table(:tasks)
  -> 0.0034s
== CreateTasks: migrated (0.0036s) ===========================

If you go look at the database you should see the tables with the columns. Now we can define the relationships between the tables. Ours is actually pretty simple: a Project has_many Tasks. So, how do we define the relationship? We do it in the Model. Go to app/models/project.rb, it should look like this

class Project < ActiveRecord::Base
end

(Oh yeah, really quick, you notice the < in the syntax above, well, Ruby is an Object Oriented language, and this syntax means Project inherits from ActiveRecord.)

So, to say that Project has many Tasks you pretty much state it

class Project < ActiveRecord::Base
  
has_many :tasks
end

This is a two way relationship, so we also edit app/models/task.rb to say that a Task belongs to a Project

class Task < ActiveRecord::Base
  belongs_to :project
end

Cool, now that we have that set up we can actually test this. Rails has an interactive development environment in the console which we can use. Type the following

script/console

You should see

Loading development environment.
>>

So, the first thing we will do is create a new Project. Rails has created the method we need to do this, it's the 'new' method. To create a new Project type the following

>> project = Project.new(:name => 'Test Project')

You should get the response

=> #<Project:0x24b614c @new_record=true, @attributes={"name"=>"Test Project"}>

We have created a Project named "Test Project". If you go look in the database you won't see anything, because we haven't called Save, instead it's just in memory at the moment. It has been created though, so you can inspect it

>> project.name
=> "Test Project"
>> project.new_record?
=> true
>> project.id
=> nil

Since it hasn't been saved in the database a Unique ID hasn't been assigned. Now, here's the cool part. Remember how we defined that a Project has many Tasks in our Model? Well, if you type the following, you get a nice surprise

>> project.tasks
=> [ ]

That's right, we never added a tasks field to the database, but we set up a relationship, and here it shows an empty array waiting for some tasks.

Now we can save it. Again, just like the 'new' there's also a 'save' method we can call.

>> project.save
=> true
>> project.id
=> 1

Now we have a Unique ID.

Now we can create a task, much like we created a project

>> task = Task.new(:name => 'Test Task 1', :due_date => Time.now, :is_complete => false)

We can't save the task yet, if you call Save you get an error

>> task.save
ActiveRecord::StatementInvalid: MySql::Error: #23000Column 'project_id' cannot be null.

We set the constraint on the project_id field that said it couldn't be null. To save the Task it needs to belong to the Project we created. We can add it using the << operand

>> project.tasks << task

Now you can call task.save and project.save

If you call project.tasks it will list all the tasks. You can access the attributes on the task like this

>> project.tasks[0].name
=> "Test Task 1"

Rails creates methods to allow you to find items in the database for creating an instance of an object. An example of this is

>> test_project = Project.find_by_name('Test Project')

now you can perform the same inspection as before

>> test_project.tasks[0].due_date
==> Sun Jan 13 22:34:17 -0500 2008

Rails actually creates a find for every column, so you can do something like the following

>> test_task = Task.find_by_project_id(1)
>> test_task.name
=> "Test Task 1"

Well, that's enough for now. I'll try not to take another month before writing up Part 3.
|