Libraries, exceptions, and debugging

Making the demo app

We’re going to actually show the problem now instead of discussing it more. We’re going to demonstrate it by setting up an extremely simple forum-esque piece of software. This is going to talk about using some rails features. If you want to work the examples, the easiest way would be to actually create a full rails app and just use script/console to play with the models.

Our forum is pretty much the simplest forum you’ve ever heard of: it has topics and posts. I guess you can argue that’s the main part, but I stress that it’s the only part. A topic has a title and a creation date. We’ll also give it a state so you could, say, lock a topic or hide a topic. A post has some content, an author, and belongs to a topic. We’ll again give it a state, though I can only think to make it visible, invisible, or possibly flagged (for moderator review or something). So you can follow along, I’ll post pertinent code. Here’s a migration to create such a setup (I apologize now for poor formatting; I’m actually doing this all in the browser since I don’t have Rails set up on this computer):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class CreateInitialSchema < ActiveRecord::Migration
  def self.up
    create_table :topics, :force => true do |t|
        t.column :title, :string
        t.column :created_on, :datetime
        t.column :state, :string, :default => 'visible'
    end
 
    create_table :posts, :force => true do |t|
      t.column :content, :text
      t.column :author, :string
      t.column :topic_id, :integer
      t.column :created_on, :datetime
      t.column :state, :string, :default => 'visible'
    end
  end
 
  def self.down
    drop_table :posts
    drop_table :topics
  end
end

We need to actually create these models. (For easier copy and pasting, I’ll post full listings for the files every time). We’re just making the models right now, so let’s set the basic models up. We’ll start with the very straight forward topic class that will just verify we don’t get multiple topics with the same name:

1
2
3
4
def Topic < ActiveRecord::Base
  has_many :posts
  validates_uniqueness_of :title
end

At this point, that’s pretty straight forward. Next is for posts which to encourage variety will have a similar uniqueness validation on the content itself in the scope of the topic (so no two people can respond to a topic the same way:

1
2
3
4
def Post < ActiveRecord::Base
  belongs_to :topic
  validates_uniqueness_of :content, :scope => :topic_id
end

If you want to go play with your new models, they should be functional at this point. They really aren’t that interesting though. What we can do to make them more interesting is to use the state_machine plugin I mentioned earlier. After all, there are certain rules that we can easily implement as a state machine on both our topics and posts.

For those of you who are not familiar with the concept of a state machine, it is a device that is described by states (think standing or sitting) and transitions between these (such as a transition called “stand up” that would take a human from sitting to standing). There are restrictions on what transitions can happen when though. It can be argued that standing up when already standing does nothing, but jumping up and down is something that should only be done when standing. So we can say you can transition from sitting to standing by stand up, from standing to sitting by sit down, and from standing to jumping by jump. This is a simple example of a silly state machine (that at this point means that once you start jumping you can’t stop…).

For our needs, we will start by describing the states of each object. Topics can either be visible (kind of a normal or default state), locked (no changing the topic or the posts in it), or hidden (doesn’t show up for visitors). Posts can be visible (again, normal and default), flagged (invisible to normal users but visible to moderators), or hidden (invisible). A topic needs to have at least one post in it to exist and at least one visible post to be visible itself. Also, posts can only be modified if the topic isn’t locked. Posts for a topic also must be hidden if a topic is hidden.

All these rules could be annoying to implement in models by default. state_machine makes it very easy to do though. We could alternatively use callbacks or just custom methods to simulate the same situation. Let’s start by creating the state machine for topics. We’ll need an extra method for dealing with hiding all the posts for the topic which we’ll define now also. (In order to make this work, you’ll need the state_machine plugin. You can get it by running script/plugin install git://github.com/pluginaweek/state_machine.git from your rails directory root).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
def Topic < ActiveRecord::Base
  has_many :posts
  validates_uniqueness_of :title
 
  state_machine :state, :initial => :visible do
    after_transition :on => :hide, :do => :hide_posts
    after_transition :on => :show, :do => :show_posts
 
    event :lock do
      transition :visible => :locked
    end
 
    event :unlock do
      transition :locked => :visible
    end
 
    event :hide
      transition any => :hidden
    end
 
    event :show
      transition :hidden => :visible
    end
  end
 
  protected
 
  def hide_posts
    this.posts.with_state('visible').each do { |post| post.hide_for_topic }
  end
 
  def show_posts
    this.posts.with_state('hidden_for_topic').each do { |post| post.unhide_for_topic }
  end
end

Events are the transitions we are looking for. Since we don’t need to define anything special for the states, we allow just the transition itself (which will update the :state property) to do the work. We did need to add a couple after_transition calls so that we can update the posts appropriately. It is important to notice that the each calls come on a scoped set of the posts, so we are only trying to ever transition validly (if you look at the transitions, they are :start_state => :end_state; since they all have one start state, starting from another will raise an exception stating “Invalid Transition”). Speaking of posts, we should go and add those methods we defined. But those aren’t (exactly) methods! Those are actually transitions. We’ll have those in our state machine also. Speaking of which, let’s define the state machine and update our Post model.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
def Post < ActiveRecord::Base
  belongs_to :topic
  validates_uniqueness_of :content, :scope => :topic_id
 
  after_destroy { |post| post.topic.destroy! if post.topic.posts.count < 1 }
 
  state_machine :state, :initial => :visible do
    # add some notification to admin after a post is flagged?
    after_transition :on => :hide, :do => :consider_hiding_topic
 
    event :flag do
      transition :visible => :flagged
    end
 
    event :approve do
      transition :flagged => :visible
    end
 
    event :hide do
      transition :visible => :hidden
    end
 
    event :show do
      transition :hidden => :visible
    end
 
    event :hide_for_topic
      transition :visible => :hidden_for_topic
    end
 
    event :show_for_topic
      transition :hidden_for_topic => :visible
    end
  end
 
  protected
 
  def consider_hiding_topic
    self.topic.hide! if self.topic.posts.with_state('visible').count < 1
  end
end

All our rules are in place! You should be able to play with your models now if you want via irb or any other means you’d like.