It’s been a while since I wrote a really useful article hahah, but with the recent launch of my website (Uhtred M.), I think this will be a nice insight to share.
A challenge I had to overcome while developing the first Social Sales Network in Angola, the Bazaar project. That’s around 2020.
The questions I asked myself at the time were: How to structure and implement a beautiful following system? How does the Twitter followers system (now X) work? How does the Instagram followers system work?
After some research, I was able to consolidate various information and implement a working system. From the research studied at the time, I was able to obtain several insights from Twitter engineers, so I ended up relying heavily on Twitter’s structure to implement Bazaar’s follower system.
This article will act as a tutorial, in it I will explain in detail how I structured and implemented the Bazaar follower system at the time, of course, adding some updates that make sense today.
Important.
The implementation will be done with Python 3.11
and Django 4.2
. You will need to have some experience with these technologies so you can understand everything.
I’m the type who likes straight-to-the-point content more, so I’ll consider that you already have a certain amount of knowledge about the technologies mentioned above; that you already know how to start a django project by isolating python dependencies with python-venv, virtualvenv or some other isolation tool; who knows django concepts such as Django ORM, Django Applications, among others.
In this article I will focus on modeling the system and creating some useful methods for relationship management.
Modeling.
For this system, we will structure two main models, User
and Relationship
. django already has a model for registering and authenticating users (django.contrib.auth.models.User
) which is part of one of django’s contribution applications.
Generally, I prefer to create an application named user
and define the user model in it for more customization freedom. After installing the application (user
), we just need to set the new model (user.models.User
) as the authentication model so that django knows we’ve changed the default user model.
In the settings.py
configuration file:
INSTALLED_APPS = [
# our apps
'user',
# django contrib
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.admin'
]
# ...
AUTH_USER_MODEL = 'user.User'
Relationship Model
The relational structure required for our system is many-to-many
. Which basically tells us that a user can follow other (multiple) users. It will be necessary to have more control over the intermediate model needed to define the many-to-many
relationship, for that, we will define the Relationship
model as described below.
In user.models.relationship.py
file:
import uuid
from django.db import models
from django.utils.translation import gettext_lazy as _
class Status(models.IntegerChoices):
BLOCKED = 0, _('Blocked')
FOLLOWING = 1, _('Following')
class Relationship(models.Model):
class Meta:
verbose_name = _('relationship')
verbose_name_plural = _('relationships')
Status = Status
by = models.ForeignKey(
'user.User',
related_name='relationship_by',
on_delete=models.CASCADE,
verbose_name=_('relationship from'))
to = models.ForeignKey(
'user.User',
related_name='relationship_to',
on_delete=models.CASCADE,
verbose_name=_('relationship to'))
status = models.IntegerField(
choices=Status.choices,
default=Status.FOLLOWING,
verbose_name=_('status'))
# other attributes/properties...
@property
def name(self) -> str:
return _('Relationship from %s to %s') % (self.by.username, self.to.username)
With this structure, whenever a relationship is created we will know the following:
- Using the
by
property, we will know who created the relationship (who started following or blocked it). - By the
to
property, we will know who received the relationship (who is being followed or blocked) - Additionally, I created the
status
property, which will be used to define the status of the relationship. Which in this case can be: Following or Blocked.
I added the status
property here just to demonstrate how we can use the relationship model to define more aspects for better management of the relationships created. For example, if your system allows private accounts, you can use the relationship model to allow private account users to accept or reject relationship requests.
This structure also tells us that: a user A can create a relationship with user B. Which does NOT imply that user B has a follower relationship with user A. As is generally the case with the Facebook friend system.
User Model
Our user model will have a structure similar to the one below.
In the user.models.user.py
file:
from typing import Union, Self
from django.db import models
from django.db.models.constraints import UniqueConstraint
from django.db.models.query import QuerySet
from django.contrib.auth.models import AbstractUser
from django.utils.translation import gettext_lazy as _
class User(AbstractUser):
class Meta:
verbose_name = _('user')
verbose_name_plural = _('users')
constraints: tuple = (
UniqueConstraint(
Lower('username'),
name='unique_user_username'),
)
name = models.CharField(
_('name'),
max_length=65,
blank=True,
default=str)
username = models.CharField(
_('username'),
max_length=24,
unique=True)
following = models.ManyToManyField(
'self',
through='Relationship',
related_name='followers',
symmetrical=False)
# outros atributos/propriedades...
What interests us most in this model is the definition of the following
property, let’s demystify this definition.
following = models.ManyToManyField(
'self',
through='Relationship',
related_name='followers',
symmetrical=False)
The following
property creates a many-to-many
relationship with the user model itself (that’s why we pass the string "self"
as the first argument), since users follow users. By the named argument through
, we tell the django ORM not to create a new default intermediate model and to use our Relationship
model.
Please note that:
following
areby
relationships. Where user is theby
property of all relationships. In other words, they are relationships that the user has created. Therefore, he is the follower or blocker.followers
(defined by therelated_name
named argument) areto
relationships. Where user is theto
property of all relationships. In other words, they are relationships that the user did not create. Therefore, he is either followed or blocked.
Finally, as stated above, the fact that user A follows user B, DOES NOT imply that user B follows user A. That is why we define the named argument symmetrical
as False
.
Helper methods.
Creating and removing relationships
It remains for us to define some methods that will be used to create, remove or filter relationships. Starting with the __relates
method. The __relates
method will be responsible for creating a new relationship between two users or updating the state of the relationship if the relationship already exists.
class User(AbstractUser):
def __relates(self, user: Self, status: Union[int,None] = None) -> Relationship:
if self == user:
raise Exception(_('A user cannot relates it\'s self'))
relation, created = Relationship.objects.get_or_create(
to=user,
by=self,
defaults={
'status': status or Relationship.Status.FOLLOWING,
'to': user,
'by': self})
if not created:
relation.status = status
relation.save()
return relation
Next, we will add more declaratively named methods such as the follow
method to create a new following relationship; the block_user
method to create a new blocked relationship; and finally the unfollow
method to remove a relationship.
class User(AbstractUser):
def follow(self, user: Self) -> Relationship:
return self.__relates(user, Relationship.Status.FOLLOWING)
def block_user(self, user: Self) -> Relationship:
return self.__relates(user, Relationship.Status.BLOCKED)
def unfollow(self, user: Self) -> None:
if self == user:
raise Exception(_('A user cannot unfollow it\'s self'))
try:
relation = Relationship.objects.get(
by=self,
to=user)
relation.delete()
except User.DoesNotExist:
raise Exception(_('Relation not found!'))
Filters, listing followers and followed.
Now that we’ve managed to create relationships, we can filter relationships to list a user’s followers, followed, and even friends. Following the principle of Twitter and Instagram, users with relationships with each other are considered friends, in other words, one follows the other.
We start with blocked ones, the method below will list all users blocked by a user.
class User(AbstractUser):
def get_blocked_users(self) -> 'QuerySet[Self]':
return self.following.filter(
relationship_to__status=Relationship.Status.BLOCKED)
The method below will be used to list a user’s followers. The method allows you to list all followers, including or excluding blocked users.
class User(AbstractUser):
def get_followers(self, include_blocked_user: bool = False) -> 'QuerySet[Self]':
if include_blocked_user:
return self.followers.all()
return self.followers.exclude(
models.Q(relationship_by__status=Relationship.Status.BLOCKED) |
models.Q(relationship_by__by__in=self.get_blocked_users()))
The method below will be used to list a user’s followed. The method allows you to list every followed, including or excluding blocked users.
class User(AbstractUser):
def get_following(self, include_blocked_user: bool = False) -> 'QuerySet[Self]':
if include_blocked_user:
return self.following.all()
return self.following.exclude(
models.Q(relationship_to__status=Relationship.Status.BLOCKED) |
models.Q(relationship_to__to__in=self.get_blocked_users()))
And finally, the method below will list all of a user’s friends. All friends, including or excluding blocked ones.
class User(AbstractUser):
def get_friends(self, include_blocked_user: bool = False) -> 'QuerySet[Self]':
if include_blocked_user:
return self.followers.filter(
relationship_by__to=self,
relationship_to__by=self)
return self.followers.filter(
relationship_by__to=self,
relationship_to__by=self,
relationship_to__status=Relationship.Status.FOLLOWING,
relationship_by__status=Relationship.Status.FOLLOWING)
As a bonus, here are some useful methods for knowing when one user dries another and some counting properties.
class User(AbstractUser):
@property
def count_following(self) -> int:
return self.get_following().count()
@property
def count_blocked_user(self) -> int:
return self.get_blocked().count()
@property
def count_friends(self) -> int:
return self.get_friends().count()
def im_following(self, user) -> bool:
return self.following.filter(id=user.id).exists()
def follows_me(self, user) -> bool:
return self.followers.filter(id=user.id).exists()
Our article ends here! Thank you for your attention and interest. Be sure to share, subscribe to our newsletter to stay on top of upcoming insights, and be sure to explore other insights.
- Subscribe to my YouTube channel, I will make a video version of this article.
- Follow me on Instagram to stay up to date with the latest news.
- Or connect with me on LinkedIn.
It was a pleasure, do not forget to share in the comments or via e-mail any questions, criticisms or suggestions regarding this article.