How to Use TimelineFu: Building an activity feed for a social application
Posted by: François Beausoleil on 2009-02-26
In this article, I would like to show how to use TimelineFu to build a timeline. This is the kind of list of events that we often see on dashboards, such as on GitHub:

I will assume you already know your way around Rails, so I won't describe the basic steps in excruciating details.
Let's start by making the application and models (I'm using Rails 2.2.2). This is a social application, with people and relationships between people as the basic entities in the domain.
$ rails myfriends $ cd myfriends $ git init $ echo "*.log" > log/.gitignore $ echo "*.sqlite3" > db/.gitignore $ echo "*" > tmp/.gitignore $ git add . $ git commit --message "Initial commit" $ script/generate scaffold person name:string $ script/generate scaffold relationship person_id:integer friend_id:integer $ rake db:migrate $ git add . $ git commit --message "Scaffolded models"
Time to write a minimal set of tests.
class PersonTest < ActiveSupport::TestCase should_have_valid_fixtures should_require_attributes :name should_allow_mass_assignment_of :name should_not_allow_mass_assignment_of :relationships, :friends, :relationship_ids, :friend_ids should_have_many :relationships should_have_many :friends end
And make those pass:
class Person < ActiveRecord::Base validates_presence_of :name attr_accessible :name has_many :relationships has_many :friends, :through => :relationships end
Turning to relationships:
class RelationshipTest < ActiveSupport::TestCase should_have_valid_fixtures should_require_attributes :friend_id, :person_id should_allow_mass_assignment_of :friend, :person should_not_allow_mass_assignment_of :friend_id, :person_id should_belong_to :person, :friend end
class Relationship < ActiveRecord::Base belongs_to :person belongs_to :friend, :class_name => "Person" validates_presence_of :person_id, :friend_id attr_accessible :person, :friend end
All pretty standard stuff, really.
Installation
Installation is pretty straightforward:
$ script/plugin install git://github.com/giraffesoft/timeline_fu.git
TimelineFu comes with a generator that will do the basics for you. The generator is completely optional. See the README for details.
$ script/generate timeline_fu
exists db/migrate
create db/migrate/20090225144815_create_timeline_events.rb
create app/models/timeline_event.rb
Let's see what TimelineFu generated for us:
class CreateTimelineEvents < ActiveRecord::Migration def self.up create_table :timeline_events do |t| t.string :event_type, :subject_type, :actor_type, :secondary_subject_type t.integer :subject_id, :actor_id, :secondary_subject_id t.timestamps end end def self.down drop_table :timeline_events end end class TimelineEvent < ActiveRecord::Base belongs_to :actor, :polymorphic => true belongs_to :subject, :polymorphic => true belongs_to :secondary_subject, :polymorphic => true end
TimelineFu's glossary / terminology
A basic timeline event looks like this:
François added you as a friend
The actor is the person or thing that did an action. The subject is what was acted against. The secondary subject is supporting documentation about the subject. And the event type is pretty self-explanatory. The example above looks like:
- Actor: François
- Subject: You (a Person instance)
- Secondary Subject: the Relationship instance
- Event Type: "added you as a friend" (friended)
Another example:
François posted a new comment on "How to use TimelineFu to build timelines"
- Actor: François
- Subject: Comment
- Secondary Subject: the Post
- Event Type: "posted a new comment" (commented)
Let's begin by writing a failing test.
class RelationshipTest < Test::Unit::TestCase context "Adding another person as a friend" do setup do @francois = person(:francois) @james = person(:james) @francois.friends << @james end should_change "TimelineEvent.count", :by => 1 end end
Run and see the test fail. Open up TimelineFu's README and look at how timeline events are created. Hint: it's called #fires.
class Relationship < ActiveRecord::Base fires :friended, :on => :create end
If you were to look at the attributes of the timeline_event as it was created, here's what you would find:
--- !ruby/object:TimelineEvent attributes: id: "1" event_type: friended actor_type: actor_id: subject_type: Relationship subject_id: "1" secondary_subject_type: secondary_subject_id: created_at: 2009-02-25 15:39:54 updated_at: 2009-02-25 15:39:54
Notice that actor and secondary subject are nil. This is because we haven't specified any options to the #fires call. Let's remedy the situation with a new set of failing tests:
class RelationshipTest < ActiveSupport::TestCase context "Adding another person as a friend" do context "the timeline event" do setup do @event = TimelineEvent.last end should "set the actor to be the person who created the friendship" do assert_equal @francois, @event.actor end should "set the subject to the relationship" do assert_equal @francois.relationships.first, @event.subject end should "set the secondary subject to the new friend" do assert_equal @james, @event.secondary_subject end should "set the event type to be 'friended'" do assert_equal "friended", @event.event_type end end end end
class Relationship < ActiveRecord::Base fires :friended, :on => :create, :actor => :person, :subject => :friend, :secondary_subject => :self end
It's nice to know when someone friends you, but isn't it even better to know when they don't want you as friends anymore? This way, you'll know not to give them gifts when it's their birthday. A new failing test:
class RelationshipTest < Test::Unit::TestCase context "Deleting the relationship" do setup do relationships(:james_to_francois).destroy end should_change "TimelineEvent.count", :by => 1 context "the timeline event" do setup do @event = TimelineEvent.last end should "set the actor to the person who destroyed the friendship" do assert_equal people(:james), @event.actor end should "set the subject to the relationship" do # It's been deleted... Oops! end should "set the secondary subject to the old friend" do assert_equal people(:francois), @event.secondary_subject end should "set the event type to 'unfriended'" do assert_equal "unfriended", @event.event_type end end end end
And the implementation:
class Relationship < ActiveRecord::Base fires :unfriended, :on => :destroy, :actor => :person, :secondary_subject => :friend end
Rendering the events feed
We need a way of getting recent events for any person. Let's analyze what we want to achieve. If someone adds me as a friend, I want that event to appear in my timeline (timeline_events WHERE subject == self). I also want to see events of my friends, such as "James added Mat as a friend" (timeline_events WHERE actor IN (my friends)). Let's write a first failing test:
class PersonTest < ActiveSupport::TestCase context "A new person" do context "where James friends self" do setup do people(:james).friends << @person end should_change "@person.recent_events.count", :by => 1 end end end
Let's do the simplest thing that could possibly work:
class Person < ActiveRecord::Base has_many :recent_events, :as => :subject, :class_name => "TimelineEvent", :order => "timeline_events.created_at DESC" end
That works just fine. But how do I get to the events of my friends? My friends events are the ones where actor_id is one of my friends. Let's write another failing test:
class PersonTest < ActiveSupport::TestCase context "A new person" do context "where James friends self" do context "where James friends someone else when James is my friend" do setup do @person.friends << people(:james) people(:james).friends << Person.create!(:name => "Daniel") end should_change "@person.recent_events.count", :to => 2 end end end end
And since we want both where subject = X or actor = X, we must use the :finder_sql option of has_many:
class Person < ActiveRecord::Base RECENT_EVENTS_CONDITION = '(subject_id = #{id} AND subject_type = \'Person\') OR (actor_type = \'Person\' AND actor_id IN (SELECT friend_id FROM relationships WHERE relationships.person_id = #{id}))' has_many :recent_events, :class_name => "TimelineEvent", :finder_sql => 'SELECT timeline_events.* FROM timeline_events WHERE ' + RECENT_EVENTS_CONDITION + ' ORDER BY timeline_events.created_at DESC', :counter_sql => 'SELECT COUNT(*) FROM timeline_events WHERE ' + RECENT_EVENTS_CONDITION end
Now we turn our attention to the user interface. How do we present this information in a nice way to the user? At giraffesoft, we do not write view tests. Views change too often for the tests to be useful. We do use Cucumber though. Anyway, do the simplest thing that could possibly work:
<%= render_timeline @person.recent_events %>
Since TimelineEvent has a field called event_type, let's use that to render a different partial.
module RenderHelper def render_timeline(events) events.map do |event| render(:partial => "timeline_events/#{event.event_type}", :object => event) end.join end end
I cannot use render :partial => events here, because all the events have the same type, namely TimelineEvent. I want to render a different partial depending on the value of event_type.
Introducing the timeline_fu Example App
The code for this article is available on github, as the timeline_fu-example application. It is a sample / starter application for rendering event feeds. Fork away!

