Build a Social Network With Django - Part 1

Build a Social Network With Django – Part 1

by Martin Breuss intermediate django web-dev

In this four-part tutorial series, you’ll build a social network with Django that you can showcase in your portfolio. This project will strengthen your understanding of relationships between Django models and show you how to use forms so that users can interact with your app and with each other. You’ll also learn how to make your site look good by using the Bulma CSS framework.

In the first part of this tutorial series, you’ll learn how to:

  • Implement one-to-one and many-to-many relationships between Django models
  • Extend the Django user model with a custom Profile model
  • Customize the Django admin interface

After finishing the first part of this series, you’ll move on to the second part, where you’ll learn to integrate Bulma to style your app and implement the front-end interface and logic so that your users can follow and unfollow each other.

You can download the code for the first part of this project by clicking the link below and going to the source_code_final/ folder:

Demo

In this four-part tutorial series, you’ll build a small social network that allows users to post short text-based messages. The users of your app can follow other users to see their posts or unfollow them to stop seeing their posts:

By the end of this tutorial series, you’ll also learn to use the CSS framework Bulma to give your app a user-friendly appearance and make it an impressive part of your web development portfolio that you can be proud to show off.

In the first part of this series, you’ll plan the project, create a base Django web app, and extend the built-in user model with a post-save hook. At the end of this part, you’ll be able to create new users through the Django admin interface and your app will automatically generate a profile for each new user and set up the necessary connection:

The back-end user implementation lays the foundation that you’ll build on in the upcoming parts.

Project Overview

In this section, you’ll get a solid idea of what you’ll build and why you’ll build it in this way. You’ll also dig into the database relationships that you’ll implement, and you’ll come up with a complete project outline ready to go. In short, you’ll set some time aside to brainstorm your project idea.

Once you’ve worked out your plan, you’ll start the practical implementation steps for the first part of the series, which focuses on Django models and their relationships:

To get a high-level idea of how you’ll work through all four parts of this series on building your Django social network, you can expand the collapsible section below:

You’ll implement the project in a series of steps spread out over four parts. There’s a lot to cover, and you’ll go into detail along the way:

📍 Part 1: Models and Relationships

  • Step 1: Set Up the Base Project
  • Step 2: Extend the Django User Model
  • Step 3: Implement a Post-Save Hook

⏭ Part 2: Templates and Front-End Styling

  • Step 4: Create a Base Template With Bulma
  • Step 5: List All User Profiles
  • Step 6: Access Individual Profile Pages

⏭ Part 3: Follows and Dweets

  • Step 7: Follow and Unfollow Other Profiles
  • Step 8: Create the Back-End Logic For Dweets
  • Step 9: Display Dweets on the Front End

⏭ Part 4: Forms and Submissions

  • Step 10: Submit Dweets Through a Django Form
  • Step 11: Prevent Double Submissions and Handle Errors
  • Step 12: Improve the Front-End User Experience

Each of these steps will provide links to any necessary resources and give you a chance to pause and come back at a later point, in case you want to take a break.

You might be eager to start programming, but before beginning to work on any coding project, it helps to think about the structure of what you want to build.

You can use pseudocode, written specifications, database diagrams, notebook scribbles, or anything that feels accessible and helps you think. Don’t skip this part! It’s an essential step for building any project. The time that you invest in planning will drastically reduce your implementation time.

So, what do you need for a social network? In its most basic form, you need two things:

  1. User-to-user connections that allow people to connect with one another
  2. Content creation and display functionality so that your users can create output for their connected users to view

You can think of these two topics as separate from each other, but you’ll need them both for your social network to function as expected.

Profiles for User-to-User Connections

For the first part of this tutorial series, you’ll need to map out how you can allow users to connect and how this translates to a database schema. This part focuses on connections.

How can you implement connections in Django models? First, you’ll jot down what a basic version of these connections could look like:

  1. You’ll have multiple users in your social network.
  2. They need to know about each other so that they can decide whom they want to follow.

For this project, you’ll implement the connections between users of your social network following a couple of assumptions that expand on the two cornerstones mentioned above:

  • Your users will be able to either follow or not follow another user.
  • If they follow someone, they’ll see that user’s content. If they don’t, they won’t.
  • Your users can follow a person without being followed back. Relationships in your social network can be asymmetrical, meaning that a user can follow someone and see their content without the reverse being true.
  • Your users need to be aware of who exists so that they know whom they can follow.
  • Users should also be aware of who follows them.
  • In your app’s most basic form, users won’t have many extra features available to them. You won’t implement a way to block people, and there won’t be a way to directly respond to content that someone else posted.

Essentially, you can think of your social network as a repository of short-form blogs or RSS feeds that users can either subscribe to or not. This is the implementation that you’ll build in this four-part series. Later on, you can build on top of this foundation to make your social network more specific and complex.

You’ll capture the functionality that you need by combining Django’s built-in User model with a custom Profile model that extends this default User model:

ER diagram showing the relationship between the built-in User model and the extended Profile model

In the graphic above, you can see a draft of an entity–relationship (ER) diagram showing that every user will have exactly one profile and that profiles can follow one another asymmetrically.

This diagram doesn’t aim to be perfect or complete. For your own process, maybe you’ll want to sketch out something slightly different on a piece of paper. You’re brainstorming, so use whatever format works best for you.

Text-Based Content

In addition to establishing relationships between users, your platform also needs a way for users to create and share content. The content could be anything. It could include images, text, videos, webcomics, and more.

In this project, you’re building a social network that handles limited-character text-based messages, similar to Twitter. Because you’ll make it using the Django web framework, it’ll carry the fashionable name Dwitter.

Your Dwitter network will need a model for storing the text-based messages that users can create, which you’ll refer to as dweets. You’ll record only three pieces of information about each dweet:

  1. Who wrote it
  2. What the message says
  3. When the user wrote it

There’s only one relationship that you need to define for your Dweet model, which is its connection to the built-in User model:

An ER diagram of both the Profile and Dweet model, and how they relate to the built-in User model

The ER diagram shows how the built-in User table connects to the Dweet table through a one-to-many relationship. This type of relationship means that one user can have many dweets and each dweet belongs to exactly one user.

You can also see the different fields in the table that correspond to the information that you want to collect about each dweet:

  1. user: holds information about who wrote the message
  2. body: holds the text content of the message
  3. created_at: holds the date and time when the user posted the message

The created_at field is grayed out in the ER diagram because you won’t allow your users to edit this themselves. Instead, Django will automatically fill the field whenever a user submits a new message.

You’ll also need a way for your users to create content and view the content that they and others in their network have created. For the convenience of your users, you’ll have to do some of the following tasks to set up the front end:

  • Provide a form for submitting content
  • Create views to handle these submissions
  • Build templates to display existing content
  • Make it look decent

Most of the topics that you’ll cover in this series are general topics that are applicable to many Django web apps. You might already know how to do some of them, but here you’ll get to explore them in the context of a completely new project. And even if you haven’t yet encountered any of the tasks involved, you’ll learn how to tackle each challenge one after another.

Now that you’ve taken some time to brainstorm your project idea, you can get ready to build it!

Prerequisites

To complete this tutorial series, you should be comfortable with the following concepts:

If you don’t have all of this knowledge before starting this tutorial, that’s okay! You might learn more by just going ahead and getting started. You can always stop and review the resources linked above if you get stuck.

Because you’ll use Django to build the back end for your social network, you’ll want to be familiar with the Django framework to get the most out of this series. If you haven’t used Django much before, you might want to try building a Django project that focuses on the basics first. For a good introduction, you can learn about Django by building a portfolio app.

Step 1: Set Up the Base Project

At this point, you know what you’ll build and you understand the database relationships that you’ll implement. At the end of this step, you’ll have set up a Django project and edited the Django admin interface to allow focused and minimalistic user creation.

You’ll tackle a few steps one after another:

  1. Create a virtual environment and install Django
  2. Create a Django project and app
  3. Customize the Django admin interface
  4. Create users for your app

Before doing anything else, you’ll first create a virtual environment and install Django.

Create a Virtual Environment and Install Django

Start by creating a new project root folder where you’ll put all the files that you’ll make while working on this project, then navigate into that folder:

Shell
$ mkdir django-social
$ cd django-social

After navigating to the parent folder where you’ll develop your project, you can create and activate a virtual environment and install Django from the Python Packaging Index (PyPI):

Shell
$ python3 -m venv venv --prompt=social
$ source ./venv/bin/activate
(social) $ python -m pip install django==3.2.5

These commands create a new virtual environment named social, activate this environment, and install Django.

Create a Django Project and App

Once the installation is complete, then you can start a new Django project named social. The name of your project doesn’t have to align with the name of your virtual environment, but this way, it’ll be more straightforward to remember.

After creating the Django project, create a new Django app called dwitter to go along with it:

Shell
(social) $ django-admin startproject social .
(social) $ python manage.py startapp dwitter

You’ll also need to register your new dwitter app to INSTALLED_APPS in social/settings.py:

Python
# social/settings.py

# ...

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "dwitter",
]

Adding the name of your app to this list will make Django aware that you want to include the app in your Django project. This step is necessary so that the edits you make in dwitter will affect your project.

Before you continue working on this tutorial, make sure that you have:

  1. An activated virtual environment
  2. A new Django project called social
  3. A Django app called dwitter
  4. Your project’s settings.py file with dwitter registered as an app

If you get stuck on any of these steps, then you’ll find all the necessary setup steps described in detail in the dedicated tutorial on how to set up Django, which is linked above.

Customize the Django Admin Interface

Django’s built-in admin interface is a powerful tool for managing your application, and you’ll use it to handle user creation and management in this tutorial.

To keep your experience with the admin interface focused on the essentials, you’ll apply some customization. Before doing that, you’ll take a look at the default state. You need to set up Django’s default SQLite database and create a superuser so you can log in to the Django admin portal:

Shell
(social) $ python manage.py migrate
(social) $ python manage.py createsuperuser
Username: admin
Email address: admin@example.com
Password:
Password (again):

After running these two commands and entering information for the superuser account, you can start Django’s development server:

Shell
(social) $ python manage.py runserver

Navigate to the /admin URL on your localhost at port 8000 and log in to the admin portal:

Django Admin dashboard view

You can see the default model entries for Groups and Users. These come from Django’s built-in authentication and user management apps. Feel free to look around if you’re not yet familiar with them.

There’s a lot in there! However, you want to keep this project as close to the fundamentals as possible to focus on model relationships and the content of your social network.

To simplify your admin, there are a few things you can cut down on:

  1. You won’t use Django’s Groups, so you can remove it from your admin view altogether.
  2. The most basic way to create a user in Django is by passing just a username. You can remove all the other fields from the Users model display as well.

Start by unregistering the Group model, which removes that model from your admin interface:

Python
# dwitter/admin.py

from django.contrib import admin
from django.contrib.auth.models import Group

admin.site.unregister(Group)

To unregister Group, first import it from django.contrib.auth.models. Then, use .unregister() to remove it from your admin display.

Check back in your admin interface and notice how the entry for Groups is gone after doing this.

Next, you’ll change the fields displayed in the admin section of Django’s built-in User model. To do this, you need to first unregister it since the model comes registered by default. Then, you can re-register the default User model to limit which fields the Django admin should display. To do this, you’ll use a custom UserAdmin class:

Python
 1# dwitter/admin.py
 2
 3from django.contrib import admin
 4from django.contrib.auth.models import User, Group
 5
 6class UserAdmin(admin.ModelAdmin):
 7    model = User
 8    # Only display the "username" field
 9    fields = ["username"]
10
11admin.site.unregister(User)
12admin.site.register(User, UserAdmin)
13admin.site.unregister(Group)

Adding this code to your admin.py file simplifies the content displayed in the admin site for User, as well as the information you need to enter when creating a new user. To recap, here’s what you’re doing in the different lines of code:

  • Line 4: You add another import where you fetch the built-in User model from django.contrib.auth.models.

  • Lines 6 to 9: You create UserAdmin, a custom class based on the imported User model.

  • Line 9: You limit the fields that the admin interface displays to only username, which is enough to create a test user.

  • Line 11: You unregister the User model that comes registered by default in the admin interface.

  • Line 12: You register the User model again, additionally passing the custom UserAdmin class you created, which applies the changes that you want.

If you now navigate to the User overview page in Home → Authentication and Authorization → Users, then you’ll notice that your admin portal displays much less information than it did before:

Reduced User model overview page in the Django Admin interface

With these customizations of your Django admin interface set up, you can now quickly create test users for your app by providing only a username for them.

Create Users for Your App

Navigate to Home → Authentication and Authorization → Users → Add user by clicking on the ADD USER button in the top right corner of the interface. Clicking on this button will take you to the default Django user model’s user creation form:

Reduced user creation form in the Django Admin interface

This reduced admin portal allows you to quickly create additional test users for your Django social network whenever you need them, and you’ll only need to provide a username for them.

Go ahead and create two additional users through this interface. You can give them any name you like, such as alice and bob. Once you’ve set up the extra users, then you’ve finished the initial setup for your project.

At this point, you’ve gotten a few essential things done:

  • You created a Django project called social.
  • You created a Django app called dwitter.
  • You cleaned up your admin portal to focus on the essential functionality you need.
  • You created a few users for your social network.

Now it’s time to think about the functionality that you want to implement in your social network. If you investigate the Django users that you just created, you might notice that the built-in User model doesn’t have any functionality that allows users to connect. To model connections between users, you’ll need to extend the default Django User model.

Step 2: Extend the Django User Model

At this point, you have a functional Django project with a few registered users. At the end of this step, you’ll have a profile for each user linked to the built-in Django User model, allowing users to connect.

You’ll need a way to hold information about the users of your app. If you started from scratch, you’d have to build an entirely new user model for that. Instead, you’re going to use the built-in Django User model to rely on Django’s well-tested implementation so that you can avoid reinventing the authentication wheel.

However, you’ll also need additional functionality that the default User model doesn’t cover: How can one user follow another user? You’ll need a way to connect users to other users.

While the built-in User model in Django is helpful, it often won’t be enough when building custom apps since it focuses on the minimum setup necessary for authentication. A great way to keep capitalizing on Django’s built-in capacities for user management, while adding your specific customizations, is to extend the User model.

In this tutorial, you’ll link two separate models using a one-to-one relationship, which is one of the officially suggested ways of tackling this challenge.

Create a Profile Model

You’ll extend Django’s built-in User model by using a one-to-one relationship with a small and focused new model, Profile. You’ll build this Profile from scratch. This Profile model will keep track of the additional information that you want to collect about each user.

What do you need in addition to the user information that the Django User model already includes? Take out your notebook and brainstorm the additional user attributes that you need for your basic social network before looking at a possible solution:

There are at least three pieces of information that you’ll need to collect about each user:

  1. Whom they follow
  2. Who follows them
  3. Which dweets they wrote

You’ll implement the first two of these in the Profile model, while you’ll save the dweets for a later part of this tutorial series.

You could add even more details about each user than what’s outlined above, such as biographical information. After you complete this tutorial series, adding more details to the Profile model will be an excellent exercise.

The Profile model only contains information that your users create after they already have a user account, which allows you to let Django handle the sign-up and authentication process. This is one of the suggested ways for extending the existing User model, so you’ll stick with it.

Your Profile model should record the user’s connections to other user profiles. That’s the fundamental piece of information that’s still missing before you can successfully model user-to-user connections. This means that your main focus with the Profile model will be setting it up to record who follows a profile and, from the other direction, whom a profile follows.

You’ll only need to create one field to model both of these connections. This is because Django can treat the profile that’s doing the following as having an inverse relationship with the profile that’s being followed:

ER diagram showing the relationship between the built-in User model and the extended Profile model

The ER diagram shows that the Profile model connects to itself through the follows many-to-many relationship.

Open models.py in your dwitter app and write the code for your new Profile model:

Python
 1# dwitter/models.py
 2
 3from django.db import models
 4from django.contrib.auth.models import User
 5
 6class Profile(models.Model):
 7    user = models.OneToOneField(User, on_delete=models.CASCADE)
 8    follows = models.ManyToManyField(
 9        "self",
10        related_name="followed_by",
11        symmetrical=False,
12        blank=True
13    )

By setting up Profile this way, you couple each profile to precisely one user:

  • Line 4: You import the built-in User model that you want to extend.

  • Line 7: You define a OneToOneField object called user, representing the profile’s connection to the user that was created with Django’s built-in user management app. You also define that any profile will get deleted if the associated user gets deleted.

  • Lines 8 to 13: You define a ManyToManyField object with the field name follows, which can hold connections to other user profiles.

  • Line 10: In this line, you pass a value for the related_name keyword on your follows field, which allows you to access data entries from the other end of that relationship through the descriptive name "followed_by".

  • Line 11: You also set symmetrical to False so that your users can follow someone without them following back.

  • Line 12: Finally, you set blank=True, which means your users don’t need to follow anyone. The follows field can remain empty.

With Profile set up, you can run Django’s database commands to propagate the model updates to your database:

Shell
(social) $ python manage.py makemigrations
(social) $ python manage.py migrate

Running makemigrations creates a migration file that registers the changes to your database, and migrate applies the changes to the database.

You can now register the Profile model to your admin interface so that the admin will display it in addition to the built-in User model:

Python
# dwitter/admin.py

# ...
from .models import Profile

# ...
admin.site.register(Profile)

After importing Profile from your current app and registering it with the admin interface, you can restart your development server:

Shell
(social) $ python manage.py runserver

Once it’s running, head over to your admin interface. Profiles shows up underneath your User model in the DWITTER panel:

Django Admin interface main page showing the newly added Profile model

If you click on + Add next to Profiles, Django will present you with a profile create view:

Django Admin interface page to create a new user profile

You need to select a User to associate with the profile. Once you’ve chosen a user from your dropdown, you can click on SAVE to create your first user profile. After that, the profile will show up as a selectable object inside of Follows:

Django Admin page showing a successfully created user profile that now appears in the list the profile can follow

You can select the new profile object Profile object (1) and click on SAVE again to follow your own user profile. If you already created other users at this point, then make sure to create a profile for them as well.

At this point, you already have all the functionality that you need to create profiles and follow other profiles. However, having User and Profile displayed in two separate places when they’re so tightly connected and minimally informative might seem inconvenient. With some more customization of your admin interface, you can improve this setup.

Display Profile Information Within the User Admin Page

Throughout this tutorial series, you’ll handle user creation through your admin. Each of your users needs a user account and a profile, and these two need to be connected.

Instead of creating the user and their profile in two different places, you’ll customize your admin interface by adding an admin inline, which allows you to edit both in one area.

Head back to dwitter/admin.py to register your new Profile model as a stacked inline instead of the standard way of registering a model. You can use the UserAdmin class that you created earlier and customize it by adding the related model as an inline:

Python
# dwitter/admin.py

# ...

class ProfileInline(admin.StackedInline):
    model = Profile

class UserAdmin(admin.ModelAdmin):
    model = User
    fields = ["username"]
    inlines = [ProfileInline]

admin.site.unregister(User)
admin.site.register(User, UserAdmin)
admin.site.unregister(Group)
# Remove: admin.site.register(Profile)

In this code snippet, you create a stacked inline for Profile by creating ProfileInline and inheriting from admin.StackedInline. You can then add ProfileInline as a member of inlines to your UserAdmin.

Finally, you’ve also removed the last line, where you previously registered the Profile model separately.

When you go to your admin interface, you’ll see that the entry for Profiles is gone from the main page. However, when you navigate to one user entry through the Users page, you’ll see the profile information next to your user information:

User change page in Django Admin interface showing the associated Profile model as a StackedInline

The information from Profile now gets displayed together with the information from Django’s built-in User model, which you restricted to only showing the username field.

That’s a significant improvement and makes handling your users through the admin interface much more convenient!

However, your profile names are currently hard to interpret. How would you know that Profile object (1) is the user profile of admin? Without descriptive information associated with your Profile model, this is hard to guess. To change that, head back to dwitter/models.py and add a .__str__() method to Profile:

Python
# dwitter/models.py

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    follows = models.ManyToManyField(
        "self", related_name="followed_by", symmetrical=False, blank=True
    )

    def __str__(self):
        return self.user.username

With this addition, you overloaded the default .__str__() method so that it returns the value of username from the associated instance of the User model. Check the results in the admin interface by reloading the page:

Profile inline after adding a under str method to the Profile model, where now you can see the associated user name show up as the profile name

After this change, the profile object connected to the admin user displays that user’s name in the Follows list.

However, none of the additional users that you’ve created so far has a profile. Eventually, you’ll want each user to have a profile that holds additional information associated with that user.

You can give your model setup a try by creating a new profile in your admin interface and associating it with a user account through your admin inline.

After you’ve created more users with associated profiles, you’ll see them populate the Follows list. You can select one or more of the profiles shown in this list. Once you click on SAVE, then the profile will start following them.

Can you come up with an idea of how you could improve this process?

Since you want each user to always have one profile associated with them, you can set up Django to do this task for you. Every time you create a new user, Django should create the fitting user profile automatically as well. Furthermore, it should be associated with that user right away. You can implement this in models.py by using a signal.

Step 3: Implement a Post-Save Hook

You now have users and profiles and a way to create and associate them with one another through your admin interface. At the end of this step, you’ll have connected them so that creating a new user will automatically create a new profile and associate one with the other. In this step, you’ll also practice interpreting Django’s error messages and finding ways to investigate and solve challenges through error-driven development.

Coordinate Users and Profiles With a Signal

You’ve extended the built-in User model, but you haven’t created an automatic connection between users and profiles. Until now, you could only create users and profiles and associate a profile with a user manually through the admin interface.

If you haven’t tried doing this manually, give it a go now. Create a new profile in the Django admin, then associate it with an existing user account.

It’d be pretty nice to associate a new profile with a user automatically when the user is created, wouldn’t it? You can make that happen with Django signals. Head back to your dwitter/models.py file.

You’ve already mapped out what you want to achieve: When you create a new user in the database, you also want to create a new profile and link it to that user.

You can implement this functionality with the help of post_save, which will call the create_profile function object every time your code executes the user model’s .save(). Note that create_profile() is a top-level function that you define outside of Profile:

Python
 1# dwitter/models.py
 2
 3from django.db.models.signals import post_save
 4
 5# ...
 6
 7def create_profile(sender, instance, created, **kwargs):
 8    if created:
 9        user_profile = Profile(user=instance)
10        user_profile.save()
11
12# Create a Profile for each new user.
13post_save.connect(create_profile, sender=User)

You’ve added additional lines of code to your models.py file:

  • Line 3: You start by importing post_save.

  • Lines 7 to 10: You write a new function called create_profile that uses created, which post_save provides, to decide whether to create a new Profile instance. Your code will only continue if the post-save signal indicates that Django created the user object successfully.

  • Line 9: Because of the one-to-one connection you set up between User and Profile, you need to pass a User object to the Profile constructor. You do that by passing instance as an argument to Profile.

  • Line 10: Here, you commit the new profile to your database with .save().

  • Line 13: You set the post-save signal to execute create_profile() every time the User model executes .save(). You do this by passing User as a keyword argument to sender.

This implementation of a post-save signal creates a new profile for each new user. You automatically associate one with the other by passing the newly created user to the Profile constructor.

You could end it here with a sparkling clean and empty profile for each new user. However, you’ll automatically add the user’s own profile to the list of profiles they follow, so each user will also see the dweets they wrote themselves.

When you venture out to learn something new, you’re likely to run into errors left and right. In the next section, you’ll practice your calm in the face of error messages and learn how you can keep moving in the right direction when you inevitably find yourself in a communication breakdown with Django.

Add Functionality Through Error-Driven Development

If you use the post-save signal exactly how you built it in the previous section, you won’t see any of your dweets when you’re actually on the dashboard that you’ll set up later. But since it can be fun to reminisce on your own personal musings and posts, you’ll change your code so that it defaults for users to follow themselves automatically when their profile is created.

It’s often hard to get code right the first time when you try to do your own thing outside of following a step-by-step tutorial. At the same time, learning how to keep going and find solutions is essential! So you’ll practice running into errors while developing this feature and learn how you can solve these challenges.

Because you want to follow your own profile immediately when it gets created, think back to the optional follows field in Profile. Maybe you could add instance in the post-save hook function through the follows keyword when you create the Profile instance:

Python
def create_profile(sender, instance, created, **kwargs):
    if created:
        user_profile = Profile(user=instance, follows=[instance])
        user_profile.save()

That seemed quick! Fire up your development server and create a new user to check whether it works as expected.

Well, not quite. You’ll end up encountering an error message:

Python Traceback
Direct assignment to the forward side of a many-to-many set is prohibited.
Use follows.set() instead.

Django didn’t know what to do with your instructions, but it tried to guess and then gave you a suggestion. Remove follows=[instance] from Profile and try to use follows.set() instead, as suggested by the error message:

Python
def create_profile(sender, instance, created, **kwargs):
    if created:
        user_profile = Profile(user=instance)
        user_profile.follows.set(instance)
        user_profile.save()

Rinse and repeat your manual test by creating another new user in your Django admin. Django still isn’t entirely satisfied with your code, but it understands a bit more, which means it’ll give you a different error message:

Python Traceback
ValueError at /admin/auth/user/add/
"<Profile: name>" needs to have a value for field "id" before this
many-to-many relationship can be used.

You might remember that you need to commit your objects with .save() before they have an entry in your database. When Django creates the database entry, it also automatically creates the id field in the entry.

In this error message, Django tells you that a User instance first needs to exist in your database so that you can use .set() to add an instance to follows. That’s an actionable tip! Maybe you can first save the new profile to your database, then add it to follows and save again:

Python
def create_profile(sender, instance, created, **kwargs):
    if created:
        user_profile = Profile(user=instance)
        user_profile.save()
        user_profile.follows.set(instance)
        user_profile.save()

Your hopes are high this time as you create a new user! However, Django gets confused yet again:

Python Traceback
TypeError at /admin/auth/user/add/
'User' object is not iterable

The error message reads as if Django is trying to iterate over instance, which refers to the newly created User object. Is that what .set() does? It’s time to check out Django’s documentation on many-to-many relationships:

Relation sets can be set:

Python
>>> a4.publications.all()
<QuerySet [<Publication: Science News>]>
>>> a4.publications.set([p3])
>>> a4.publications.all()
<QuerySet [<Publication: Science Weekly>]>

(Source)

It seems like .set() really requires an iterable input, which Django tried to tell you in the error message. So you might be able to pass a list with instance as a single item, just like the Django documentation tells you to:

Python
def create_profile(sender, instance, created, **kwargs):
    if created:
        user_profile = Profile(user=instance)
        user_profile.save()
        user_profile.follows.set([instance])
        user_profile.save()

This change should allow .set() to iterate over the list you’re passing and add the profile associated with your new user account to the list of accounts that the user is following. Fingers crossed, you try yet again to create a new user, but another error comes up:

Python Traceback
TypeError at /admin/auth/user/add/
TypeError: Field 'id' expected a number but got <User: name>.

That’s great! Your error messages keep changing, which gives you additional information. You’re on the right path.

Now Django is telling you that it got a User object but expected an id field instead. You set up profiles to follow other profiles, yet Django is only looking for the Profile object’s .id. After extending the User model, you can access a user’s profile through their user instance with .profile and then step deeper into the object to get .id:

Python
def create_profile(sender, instance, created, **kwargs):
    if created:
        user_profile = Profile(user=instance)
        user_profile.save()
        user_profile.follows.set([instance.profile.id])
        user_profile.save()

Time for another spin. You use the admin interface to create a new user—for example, martin—and now it works:

Django Admin interface showing a successful creation of a new user that automatically created also a profile for the user, and followed itself

Congratulations! You set up your post-save hook so that a newly created user automatically follows their own profile. That same post-save hook also automatically created the profile.

While making this functionality, you also practiced staying calm in the face of repeated error messages. You improved your understanding of how Django communicates with you when misunderstandings occur.

You could have solved this challenge differently, following the suggested way of adding single objects to a many-to-many relationship using .add() instead of .set(). Search the many-to-many relationships documentation and refactor your code so it works using .add().

You can refactor your code to use .add() instead of .set(), which is the recommended way to add a single instance:

Python
def create_profile(sender, instance, created, **kwargs):
    if created:
        user_profile = Profile(user=instance)
        user_profile.save()
        user_profile.follows.add(instance.profile)
        user_profile.save()

You might also notice that you don’t need to pass .id of the linked Profile object, but that you can pass the whole object instead. This also works for the previous solution, but for that one you followed Django’s error message, which pointed you toward using .id.

All of these solutions work! Keep in mind that there’s always more than one way to solve a task in programming. It’s most important to get it to work, but it’s good practice to then revisit and refactor your code.

In this case, because you’re always adding only one instance to the set, you don’t need to iterate. This means that it’ll be more efficient to use .add() instead of .set(). Also, since it has the same effect and is more descriptive, you can pass instance.profile directly instead of using .id.

With this change, you’ve solved the issue that your users wouldn’t see their own dweets on their dashboard. Now their dweets will show up together with those of everyone else that they’re following.

At this point, you could leave this code as is, but Django also provides a more elegant way to register signals by using a decorator. In the next section, you’ll refactor your post-save hook using receiver.

Refactor Your Code Using a Decorator

Django comes with a receiver decorator, which allows you to make the code you wrote more concise, without changing its functionality:

Python
 1# dwitter/models.py
 2
 3from django.db.models.signals import post_save
 4from django.dispatch import receiver
 5
 6# ...
 7
 8@receiver(post_save, sender=User)
 9def create_profile(sender, instance, created, **kwargs):
10    if created:
11        user_profile = Profile(user=instance)
12        user_profile.save()
13        user_profile.follows.add(instance.profile)
14        user_profile.save()
15
16# Remove: post_save.connect(create_profile, sender=User)

With this refactoring, you’ve applied three changes to your codebase:

  1. Line 4: You add an import for receiver from django.dispatch.

  2. Line 8: You apply the decorator to create_profile, passing it post_save and passing the User model to sender. By passing the model, you associate post_save with the events related to the User model the same way you did with .connect() before.

  3. Line 16: You delete the line of code that previously connected post_save with User as the sender since you’ve already made that association through the arguments provided to your decorator on line 8.

Remember that you need to save the newly created Profile object first to will it into existence. Only then can you add the newly created user’s .profile to your new user_profile. Finally, you need to save the changes a second time to propagate the updated association to your database.

Congratulations, you’ve successfully set up most of the back end of your Django social network. You implemented the model relationships between your users and your Django social network profiles! Every time you create a new user, they’ll also receive a user profile and immediately follow their own profile. Additionally, user profiles can follow each other. But does it work?

Confirm the Automatic Association in Your Admin

Head back to your admin interface and create a new user through the provided form. All you have to do is provide a username and click on save. For example, you could add another user called rainn:

Creating a new user object with additional profile and follows

When you inspect the user’s Change page, you’ll see that Django automatically created a profile for the new user and added that profile to the list of profiles they follow:

Admin interface showing the new user's change page where their newly created profile is highlighted, indicating that the profile is following itself

You can see that a user follows their own profile because their profile name in the Follows list has a gray background.

You can now create new users through your Django admin interface, and they’ll automatically receive an associated profile as well. Django will also set up their profile to follow itself, which will make it possible to display their own dweets next to other people’s dweets on their dashboard.

You can change which profiles a user follows by selecting or unselecting the profile names in the Follows list and clicking on SAVE. To select multiple profiles or unselect a specific profile, you need to hold Ctrl on Windows and Linux, or Cmd on macOS, while clicking on the profile name.

But there’s not much to see here yet. Up to now, you’ve only been lurking in the Django admin. Maybe you’re starting to get tired of the pre-built pages that Django provides and itching to write some Django templates to see how you can display these model relationships in the user-facing interface of your app.

That’s what you’ll work on in the second part of this tutorial series! You’ll start building the front end of your web app, and you’ll learn how to make it look good using the CSS framework Bulma.

Conclusion

Congratulations! At this point, you’ve completed the first part of this tutorial series on building a basic social network with Django.

In the first part of this tutorial series, you learned how to:

  • Implement one-to-one and many-to-many relationships between Django models
  • Extend the Django user model with a custom Profile model
  • Customize the Django admin interface

You’ve also gained additional practice in setting up a Django project and the tasks associated with doing so. You’ve practiced reading Django error messages and finding solutions based on the information they give you.

You can download the code for the first part of this project by clicking the link below and going to the source_code_final/ folder:

Next Steps for Your Basic Social Network With Django

Now that you’ve completed the first part of this series, you can continue to part two, where you’ll build a Django front end styled with Bulma. In the second part of the tutorial series, you’ll build front-end pages for your user profiles and make them look good.

As you continue to work on this project, you can refer back to this part of the tutorial series, consult the plan that you drafted in the project overview, and update your plan along the way.

🐍 Python Tricks 💌

Get a short & sweet Python Trick delivered to your inbox every couple of days. No spam ever. Unsubscribe any time. Curated by the Real Python team.

Python Tricks Dictionary Merge

About Martin Breuss

Martin likes automation, goofy jokes, and snakes, all of which fit into the Python community. He enjoys learning and exploring and is up for talking about it, too. He writes and records content for Real Python and CodingNomads.

» More about Martin

Each tutorial at Real Python is created by a team of developers so that it meets our high quality standards. The team members who worked on this tutorial are:

Master Real-World Python Skills With Unlimited Access to Real Python

Locked learning resources

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

Master Real-World Python Skills
With Unlimited Access to Real Python

Locked learning resources

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

What Do You Think?

Rate this article:

What’s your #1 takeaway or favorite thing you learned? How are you going to put your newfound skills to use? Leave a comment below and let us know.

Commenting Tips: The most useful comments are those written with the goal of learning from or helping out other students. Get tips for asking good questions and get answers to common questions in our support portal.


Looking for a real-time conversation? Visit the Real Python Community Chat or join the next “Office Hours” Live Q&A Session. Happy Pythoning!