Opensource Development and Life
RSS icon Email icon Home icon
  • Libraries, exceptions, and debugging

    Posted on June 26th, 2009 Quad341 No comments

    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.

    Leave a reply