Activity Feeds with Rails

Activity feeds are all over the place. For example, Github’s main page lists all of the activity in your social coding network: one of your friends created or cloned a repo, a new issue was opened, your repo was forked or starred, etc.

Twitter displays the latest tweets, retweets, and replies.

That’s a convenient and easy way for users to learn about recent updates. How about trying to replicate the same functionality with Rails? Turns out, there is a convenient gem that makes the development process very easy!

In this article, I am going to talk about public_activity, a gem created by Piotrek Okoński to easily track activity for models (and more, actually). I will show you how to easily create an activity feed and employ various features of public_activity to extend the app further.

Before proceeding, I wanted to make a small note. Some months ago, I wrote an article Versioning with PaperTrail that covered paper_trail, a gem used to implement version control of an application’s models. In some ways, paper_trail is similar to public_activity and could be used to implement a solution similar to the one presented in this article. However, paper_trail is aimed at crafting versioning systems, whereas public_activity was created specifically to implement activity feeds.

Preparing the App

Suppose we want to create an app that will allow users to share (cool) stories. Users should be able to sign in, but guests will be allowed to post stories, as well. There will also be a “Like” button for each story. The most important piece of functionality, however, will be an activity feed, so users are able to see what has happened recently. Example activities in the feed are a story was added, deleted, or liked.

Time for the ground work. Let’s call our app Storyteller:

$ rails new Storyteller -T

For this demo I am using Rails 4.2.0, but the same solution (with only a few tweaks) can be implemented with Rails 3.

bootstrap-sass and autoprefixer-rails are completely optional – the first one is used for styling and the second one automatically adds browser vendor prefixes to CSS rules. public_activity is the star today, as it will help us set up the activity feed. omniauth-facebook will be used to set up authentication.

The /auth/:provider/callback is the callback route used by Facebook as a part of the OAuth2 sign in process. The :provider piece means that you may use any other Omniauth authentication strategy (or multiple strategies at once).

To obtain the Facebook key and secret, visit the developers.facebook.com page and create a new application (with a type “Website”). On the Dashboard page of the newly created app, find the “App ID” and “App Secret” (a password will have to be provided to display this one) – this is what you need. Note: The key pair should not be available publicly – I am using environmental variables to store it.

While on Facebook Developers page, navigate to the Settings page and click the “Add Platform” button, then select “Website”. Next, fill in the following fields:

Site URL (if you are testing the app on local machine, enter “http://localhost:3000”)

App Domains (if you are on local machine, leave this blank)

Contact Email

Click “Save Changes”.

Lastly, navigate to “Status & Review” and toggle the “Do you want to make this app and all its live features available to the general public?” to “Yes”. Your app is now live and users may log in using it.

The scope parameter specifies what actions to allow our application to perform. For this case, we only need to fetch basic information about the user that is logging in.

The next step is creating the controller that will handle log in and log out requests:

We are storing the necessary information and returning the user object as a result. The find_or_initialize_by method will either create a new user or update an existing one if the uid is already present in the database. This is done to prevent the same user being created multiple times.

And, lastly, the current_user method that will return the currently logged in user or nil:

helper_method ensures that this method can be called from the views as well.

When you are done, boot up the server. Try to authenticate and add a couple of stories to check that everything is working. We are ready to move to the interesting part.

Integrating public_activity

public_activity‘s idea is quite simple: use callbacks to automatically store information about the changes that happen in the specified table. That information is then used to display all the recent activity. You might be wondering if it is possible to record activity without actually touching the table. Well, it is, and we will talk about it more in a bit.

For now, let’s do the basic setup. Run these commands:

$ rails g public_activity:migration
$ rake db:migrate

This will generate and apply public_activity‘s migration. A new table called activities is created.

To enable tracking for the Story model:

models/story.rb

[...]
include PublicActivity::Model
tracked
[...]

Pretty simple, isn’t it? Now, whenever you perform actions such as save, update_attributes, destroy, and others, public_activity‘s callback will be fired to record that event.

This gem also supports MongoMapper and Mongoid adapters – refer to the Database setup section in the docs to learn more.

Displaying the Activity Feed

Let’s display the events. You may either create a separate page to display the activity feed or render it
on each page of your site. I’m going to stick with the latter option.

As you can see, the Activity model is inside the PublicActivity namespace to prevent naming collisions. We are ordering activities by creation date (the newest the first) and taking the first twenty of them. Read more here.

Our layout needs to be changed a bit so that the activity feed is being placed on the right side of the website:

For those who don’t know, Bootstrap uses a 12-column grid layout, so by specifying col-sm-9 we are using 9 columns (75 % of the available space) for the main content. col-sm-3, in turn, leaves 3 columns for the activity feed. sm here means that columns will be displayed one beneath another (horizontal grid) on smaller displays. More information is available here.

Reload the page to check what the @activities array contains and how it displays. To render an array of activities there is a special helper method render_activities:

views/shared/_activities.html.erb

<div class="col-sm-3">
<%= render_activities @activities %>
</div>

public_activityexpects that there is a public_activity folder inside the views that, in turn, has a story folder (or any other folder with a singular name of the model that is related to the specific activity). Inside the story directory, there should be the following partials: _create.html.erb, _update.html.erb, _destroy.html.erb. Each partial, as you’ve probably guessed, is rendered for the corresponding action. Inside those partials there is a local variable activity (aliased as a) available.

The trackable is a polymorphic association which has all the necessary information about the model that was modified.

There is a problem, however. If you create and then delete a story, you’ll see an error undefined method 'title' for nil:NilClass. This is because the we are trying to fetch the title of the record that was deleted. It is easy enough to fix:

views/public_activity/story/_create.html.erb

<li class="list-group-item">
<% if a.trackable %>
<%= a.trackable.title %> was created.
<% else %>
An article that is currently deleted was added.
<% end %>
</li>

views/public_activity/story/_update.html.erb

<li class="list-group-item">
<% if a.trackable %>
<%= a.trackable.title %> was edited.
<% else %>
An article that is currently deleted was edited.
<% end %>
</li>

views/public_activity/story/_destroyed.html.erb

<li class="list-group-item">
An article was deleted.
</li>

Pretty nice, but not very informative. When did the action take place? Can we navigate directly to the article that was modified? Who modified it? Well, the first two issues can be fixed easily:

I’ve also added some Bootstrap Glyphicons so that everything looks a bit prettier.

Displaying information about the user responsible for the change involves a bit more work.

Storing Information About the User

There is a special field called owner in the activities table that is intended to store information about the user responsible for the action. The problem, however, is that the current_user method is not available inside the model so we have to use a pretty hacky solution.

Note that I’ve removed the private keyword because otherwise we won’t be able to call current_user from inside the model. Adding hide_action :current_user makes sure that this method is not considered a controller action.

The key fields contains the string in the form of . so for example in our case it might be story.create, story.update or story.destroy. Of course, Bootstrap applies no styles to classes like glyphicon-create but it can be easily changed:

We are employing Sass’ @extend directive to apply the styles to our new classes.

The issue with the text can be solved with the help of fallbacks. As we’ve already seen, public_activity by default will search for the partials inside the public_activity/ directory. However if we provide the display: :i18n option, I18n translations will be used instead.

The previously created public_activity folder may be removed completely.

This will only work, however, for a single trackable model. For multiple models you will have to create the corresponding partials. It is possible to create a separate layout (which actually is yet another partial) for your activities. Here is a demo app created by the author of public_activity that uses a layout to render activities.

Create Custom Activities

Until now, we only worked with the basic CRUD operations that caused activities to be saved automatically. But what if we wanted to track some custom events? Or if there is a need to trigger an activity without touching the model?

Not to worry, this can be done quite easily. Suppose we want to add the “Like” button and count likes for each post. Moreover, a special activity should be recorded, too.

@story.increment!(:likes) just adds 1 to the likes and saves the result to the database. @story.create_activity :like actually creates a new activity by providing the like key (we’ve talked about keys before when refactoring partials). This will require us to modify the translations file:

config/locales/en.yml:

en:
activity:
story:
like: 'has liked the story'
[...]

If you are dealing with partials instead, then you’ll have to create the views/public_activity/story/_like.html.erb partial.

The create_activity method is called to trigger a custom activity – it does not require a model change.

We are not done yet, however. There is one problem that will allow me to show one more feature of public_activity – disabling model tracking. You see, the @story.increment!(:likes) code fires an update which causes public_activity to record an update event. So, @story.create_activity :like will result in recording two activities for one action. This is obviously not what we want. The first operation should be done without any tracking at all.

public_activity allows disabling tracking globally or for a specific model. For global disabling, use

PublicActivity.enabled = false

To disable tracking on model’s level use

Story.public_activity_off

We are going to employ the latter solution, as the first one is obviously overkill:

Ilya Bodrov is personal IT teacher, a senior engineer working at Campaigner LLC, author and teaching assistant at Sitepoint and lecturer at Moscow Aviations Institute. His primary programming languages are Ruby (with Rails) and JavaScript. He enjoys coding, teaching people and learning new things. Ilya also has some Cisco and Microsoft certificates and was working as a tutor in an educational center for a couple of years. In his free time he tweets, writes posts for his website, participates in OpenSource projects, goes in for sports and plays music.

Good stuff! As someone who has built and worked with activity streams for years, I will say this is a good start. One thing I would suggest looking at is the Activity Streams protocol specification - http://activitystrea.ms. This provides a standard data format for activity stream data that can then be used with any standardized activity streams tool.

Once you start to move beyond simple streams - e.g. to add different types of activity, enable commenting, and especially allow following and building of feeds, it's my belief that you shouldn't try to build it yourself. It starts to get very complex to enable that level of functionality and maintain any kind of performance. I actually built a company around solving this problem - it's called Collabinate (http://www.collabinate.com). We provide an API that allows you to build streams for any object in an application, manages the relationship of followers to streams, lets you pull back feeds in real time, enables likes and comments, utilizes the Activity Streams protocol, and does it at huge scale if necessary (billions of objects). We should talk some time if you're interested in learning more about it.

Your service seems like a nice solution - I may be interested in writing an article about it. Could you contact me via e-mail (http://radiant-wind.com - e-mail and some other contact options are listed here) if that suits you?

By the way, I have couple of questionsFirst one, if you have 2 users type (user and admin) how do you set the owner?Second, how do you filter them on the render activity or the view?and lastly, this is applicable to many models?

By the owner field, if I understood the question correctly. It has the polymorphic association (https://github.com/pokonski/public_activity/blob/4ea9f56628a7699370a6a3daad31ddf4b076a321/lib/public_activity/orm/active_record/activity.rb#L13) and you can use it for find only activities for admin or user for example.

AWESOME article! Really gave me a great grasp on the activity_feed. Is there a way to 1) limit liking to one time per post so that if you click on "Like it" a second time it unlikes it, like Facebook 2) show names of likers next to like button so you know who is liking it without having to scroll down the activity feed? Might be a nice expansion to your wonderful tutorial because I can't find these answers anywhere!

1&2. Of course that is possible, but this is not related to activity feeds so I did not consider add such info to the currect article. However I think that is a nice suggestion for a new topic so I'll probably work on this in some time, maybe expanding a bit further. It is really great to receive so many feedback!