Customized Activity Feeds in Rails
For a personal project in my third module at the Turing School (Turing’s program consists of four 6-week modules), I built an application that would allow beginning programmers to set goals and track practice sessions for various programming skills they were working to develop. The app was intended as a motivation and productivity tool for beginning programmers.
As part of the app, I wanted to help users feel like they were part of a community of learners. I also wanted users to see the practice sequences of other users, in order to help them direct their own learning. During the two week project, I was able to incorporate an activity feed that showed the activity of all users using the Public Activity gem. However, I wanted to take this a step further.
Specifically, I wanted users to be able to follow, and be followed by, like-minded users — users working on similar goals or skills, etc. And I wanted users to get a customized feed of activity performed by the users they were following.
This addition required me to make three primary changes:
- Introduce the concept of followers to the business logic — using a self-referential relationship.
- Customize the activity feed to the user — showing only the activity of the people that the user was following.
- Allow users to search for other users by username and keywords, and to follow/unfollow individual users.
In this post, I will cover some of the key aspects of implementing the first two additions.
So… Followers are just Users
Followers weren’t a new “thing” that needed a model or database table. They were just users. But I needed to treat them like a “thing” — I needed to be able to call user.followers and user.users_followed and get a collection of users back.
Thankfully, we had a memorable lesson at Turing on self-referential relationships, where we built a binary search tree in Rails. In a binary search tree, each node has a left node and a right node, which are instances of other nodes. In Rails 5, it looked like this (note that ApplicationRecord is a Rails 5 thing — ActiveRecord::Base for Rails 4 and earlier):
class Node < ApplicationRecord
belongs_to :right, class_name: Node
belongs_to :left, class_name: Node
end
In my case, I needed to add a UserFollower model that carried references to a User and Follower. And, it needed to know that the Follower was just an instance of User:
class UserFollower < ApplicationRecord
belongs_to :user
belongs_to :follower, class_name: User
end
With that addition, I could get what I needed from the User model (i.e. user.followers and user.followed_users):
class User < ApplicationRecord
has_many :user_followers
has_many :followers, through: :user_followers def followed_users
User.joins(:user_followers)
.where("user_followers.follower_id = #{self.id}")
endend
Who “owns” the activity?
However, now I needed to figure out how to get a customized activity feed. My activity feed shows activity related to skills, goals, and practice sessions. Any create action on these three models is incorporated into the activity feed. When I was showing a feed of all users, all I needed on these models was the following:
class Skill < ApplicationRecord
include PublicActivity::Model
tracked
end
Now, I needed to limit the activities to a selection of users. Fortunately, Public Activity objects carry owner_type and owner_id attributes. These attributes default to nil, unless an owner is established on the model. I simply needed to give skills, goals and practice sessions an owner. In essence, I needed activities to be owned by their associated user.
Easy enough:
class Skill < ApplicationRecord
include PublicActivity::Model
tracked owner: :user
belongs_to :user
end
However, while skills belong to a user in my business logic, goals and practice sessions belong to a skill. Notably, I can’t get from goals or practice sessions back to a user, because of the direction of the relationship. That is, a skill does not hold on to a reference to a goal or a practice session. So goal.skill.user does not work, and belongs_to :user, through: : skill is simply not a thing.
But, in order to establish a user as an owner of a goal activity, I needed to get there. Fortunately, I was able to use the delegate method to achieve this result:
class Goal < ApplicationRecord
include PublicActivity::Model
tracked owner: :user
belongs_to :skill
delegate :user, to: :skill
end
And, with that, I can customize an activity log for the user. The following method gives me the 20 most recent activities performed by users that the current user is following:
PublicActivity::Activity.order("created_at desc")
.where(owner_type: "User",
owner_id: current_user.users_following.map {|u| u.id})
.limit(20)