Topic: How to create a "Recent Activity" page with multiple models
Many web apps have a "dashboard" or "recent activity" page that shows what has changed over a certain time period. I'm going to describe how I implemented such a page in a Rails project.
Imagine an application with WorkOrder and Comment models. A WorkOrder has many Comments, and a Comment belongs to a WorkOrder. A help desk system might use a similar design, where a support person creates a new WorkOrder with a description of the problem, and it is updated with comments until it is marked as completed. The Comment model uses Rail's built-in timestamps, but the WorkOrder model has both start_date and completed_date fields.
Our "recent activity" page needs to display all started work orders, completed work orders and posted comments in chronological order over a specified period of time. How can we do this?
First, let's use three separate find calls to gather up our objects. We'd do this in our controller:
started_work_orders = WorkOrder.find(:all, :conditions => ["start_date >= ?", 3.days.ago])
completed_work_orders = WorkOrder.find(:all, :conditions => ["completed_date >= ?", 3.days.ago])
comments = Comment.find(:all, :conditions => ["created_at >= ?", 3.days.ago])
Notice I'm not using a single find call on WorkOrder with an "or" condition. That's because we'd have no way to tell whether any particular WorkOrder was in the list because it was opened over the last three days or because it was closed! Since we are going to combine these into a single list (so we can sort all activity chronologically), we need a way to determine which list a particular WorkOrder came from. For this we can use a virtual attribute that isn't saved to the database using the attr_accessor method. In the WorkOrder.rb model, add:
attr_accessor :sort_using_start_date
Now we'll set this virtual attribute to either true or false so we can refer to it later:
started_work_orders.each { |wo| wo.sort_using_start_date = true }
completed_work_orders.each { |wo| wo.sort_using_start_date = false }Great! Now we can combine all our matching objects into a single array and store it in an instance variable for our view:
@recently_active_objects = started_work_orders + completed_work_orders + comments
Next step: sort the list! But how? A WorkOrder will need to sort via start_date or completed_date, and a Comment will need to use created_at. One solution to this problem is to have all the objects provide their sort time via the same method. We'll call it sort_timestamp. Adding it to the Comment.rb model is simple enough:
def sort_timestamp
self.created_at
end
For the WorkOrder.rb model, we need a bit of logic to make sure we return the correct date:
def sort_timestamp
self.sort_using_start_date ? self.start_date : self.completed_date
end
Now all the objects in our @recently_active_objects array will respond to the sort_timestamp method. The sort becomes easy:
@recently_active_objects = @recently_active_objects.sort { |x,y| y.sort_timestamp <=> x.sort_timestamp }Notice the positions of x and y around the <=>. This will sort in reverse (so the latest objects appear first in the list). If you want the opposite behavior, just reverse x and y.
The only tricky part left is to identify each object as you loop through the array in your view, so you can display the appropriate information. I suggest creating a helper method like this:
def activity_message_for(object)
case object.class.name
when 'WorkOrder'
object.sort_using_start_date ? "Work Order #{object.id} was started" : "Work Order #{object.id} was completed"
when 'Comment'
"A comment was added to Work Order #{object.work_order.id}"
end
end
Your view would then iterate through @recently_active_objects, display a formatted version of each element's sort_timestamp value, and call activity_message_for(object) to get an appropriate activity message.
If you ever need to add additional models to your activity page, just make sure they implement a sort_timestamp method and modify the activity_message_for helper.
EDIT: Fixed case statement in activity_message_for
Last edited by jeffj312 (2008-03-11 16:56:12)