Wed, August 30, 2023 at 07:27 PM

820 views

How to model a following system similar to Twitter and Instagram with Python and Django

PythonDjangoProgrammingTutorial
insight cover

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:

  1. following are by relationships. Where user is the by property of all relationships. In other words, they are relationships that the user has created. Therefore, he is the follower or blocker.
  2. followers (defined by the related_name named argument) are to relationships. Where user is the to 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.

Get more great insights!

Or get in touch with me if you want to share any insights about business, technology, freelancing, design, among others.