Building a Social Media Site With Python and Django: Part 5 Profiles

LegionScript
6 min readFeb 11, 2021

Video Tutorial

Code on Github

In this tutorial, we are going to add profiles to our app. We will add a way to view and edit profiles from a link in the navbar. We will also use Django signals to automatically create profiles once a user is created. Let’s get started with updating the models.py file in our social app.

Setting up the Profile Model

social/models.py:

First we will add a UserProfile model, this will hold a foreign key in the form of a one to one relationship with the user model. We will also hold some extra data on the user here. Finally we will use the ImageField to hold a profile image.

class UserProfile(models.Model):
user = models.OneToOneField(User, primary_key=True, verbose_name='user', related_name='profile', on_delete=models.CASCADE)
name = models.CharField(max_length=30, blank=True, null=True)
bio = models.TextField(max_length=500, blank=True)
birth_date = models.DateField(null=True, blank=True)
location = models.CharField(max_length=100, blank=True, null=True)
picture = models.ImageField(upload_to='uploads/profile_pictures/', default='uploads/profile_pictures/default.png', blank=True)

Make sure to migrate the changes to the database. Now that we have our models added, we need to add the profiles to the admin page. Make sure you add a profile for each model or you might get an error later on. Now that we have all of that set up, let’s set up the media so we can store our profile pictures. We need to make sure that the path specified in the models is the same.

social/admin.py:

from django.contrib import admin
from .models import Post, Comment, UserProfile
admin.site.register(Post)
admin.site.register(Comment)
admin.site.register(UserProfile)

Setting up Media

First we need to add some changes to our settings.py to set up or media paths.

social/settings.py:

MEDIA_ROOT = BASE_DIR / 'media'
MEDIA_URL = '/media/'

Now we need to make a directory that matches our path set in the UserProfile model with media added on to the front of it. In our case it will look like this: media/uploads/profile_pictures/ make sure to create this directory in your file system as well. Now let’s add a couple lines to the end of our urls.py

from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
path('accounts/', include('allauth.urls')),
path('', include('landing.urls')),
path('social/', include('social.urls')),
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Now with all of this set up, we should be ready to create our Profile view.

Creating the Profile View

social/views.py:

class ProfileView(View):
def get(self, request, pk, *args, **kwargs):
profile = UserProfile.objects.get(pk=pk)
user = profile.user
posts = Post.objects.filter(author=user).order_by('-created_on')
context = {
'user': user,
'profile': profile,
'posts': posts,
}
return render(request, 'social/profile.html', context)

social/templates/profile.html:

{% extends 'landing/base.html' %}{% block content %}
<div class="container">
<div class="row mt-5">
<div class="col-md-3 col-sm-6">
<a href="{% url 'post-list' %}" class="btn btn-light">Back to Feed</a>
</div>
</div>
<div class="row justify-content-center mt-5">
<div class="card col-md-8 col-sm-12 shadow-sm px-5 pt-3">
<img src="{{ profile.picture.url }}" class="rounded-circle" width="100" height="100" />
{% if profile.name %}
<h3 class="py-4">{{ profile.name }}
{% endif %}
<div>
{% if profile.location %}
<p>{{ profile.location }}</p>
{% endif %}
{% if profile.birth_date %}
<p>{{ profile.birth_date }}</p>
{% endif %}
{% if profile.bio %}
<p>{{ profile.bio }}</p>
{% endif %}
</div>
</div>
</div>
{% for post in posts %}
<div class="row justify-content-center mt-5">
<div class="col-md-8 col-sm-12 border-bottom position-relative">
<p><strong>{{ post.author }}</strong> {{ post.created_on }}</p>
<p>{{ post.body }}</p>
<a class="stretched-link" href="{% url 'post-detail' post.pk %}"></a>
</div>
</div>
{% endfor %}
</div>
{% endblock content %}

social/urls.py:

from django.urls import path
from .views import PostListView, PostDetailView, PostEditView, PostDeleteView, CommentDeleteView, ProfileView, ProfileEditView
urlpatterns = [
path('', PostListView.as_view(), name='post-list'),
path('post/<int:pk>/', PostDetailView.as_view(), name='post-detail'),
path('post/edit/<int:pk>/', PostEditView.as_view(), name='post-edit'),
path('post/delete/<int:pk>/', PostDeleteView.as_view(), name='post-delete'),
path('post/<int:post_pk>/comment/delete/<int:pk>/', CommentDeleteView.as_view(), name='comment-delete'),
path('profile/<int:pk>/', ProfileView.as_view(), name='profile'),
path('profile/edit/<int:pk>/', ProfileEditView.as_view(), name='profile-edit'),
]

The profile is pretty similar to other views we have already created, we are using our View class so we need to create a method for each HTTP method, in this case all we need is a get method. In the template, we are listing out all of the data from the profile model, we are also listing out all of the posts made by this user. In the url, we are passing in the profile primary key so that we can differentiate between different profiles.

Updating the Navbar

Now let’s update our navbar so that we have a way to access the profile. We have a link in there right now for the profile that currently doesn’t go anywhere, let’s make that go to the profile link we just made.

landing/templates/navbar.html:

<div class="container gx-0">
<nav class="navbar navbar-expand-lg navbar-light">
<div class="container-fluid">
<a class="navbar-brand"
{% if user.is_authenticated %}
href="{% url 'post-list' %}"
{% else %}
href="{% url 'index' %}"
{% endif %}
>
<i class="fas fa-comment"></i>
Social Network
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarTogglerDemo02" aria-controls="navbarTogglerDemo02" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarTogglerDemo02">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
</ul>
<form class="d-flex">
<div class="input-group">
<span class="input-group-text" id="basic-addon1">@</span>
<input type="text" class="form-control" placeholder="Username" aria-label="Username" aria-describedby="basic-addon1">
</div>
</form>
{% if user.is_authenticated %}
<div class="nav-item dropdown">
<a class="nav-link dropdown-toggle text-dark" data-bs-toggle="dropdown" href="#" role="button" aria-expanded="false"><i class="fas fa-user"></i></a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="{% url 'profile' user.profile.pk %}">Profile</a></li>
<li><a class="dropdown-item" href="{% url 'account_logout' %}">Sign Out</a></li>
</ul>
</div>
{% endif %}
</div>
</div>
</nav>
</div>

Automatically Creating Profiles on Registration

Now that we have our profile all set up, we need to automatically create a profile once the user signs up. We can do this with Django signals. All we need to do is add a few functions to our models.py. We can use a signal called post_save which will run automatically after whatever model specified is saved in the database. In our case, the User model is saved everytime the User is saved we want to make the profile. We can add two signal functions to the bottom of our models file and it should start creating profiles on user registration:

social/models.py:

from django.db import models
from django.utils import timezone
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.dispatch import receiver
class Post(models.Model):
body = models.TextField()
created_on = models.DateTimeField(default=timezone.now)
author = models.ForeignKey(User, on_delete=models.CASCADE)
class Comment(models.Model):
comment = models.TextField()
created_on = models.DateTimeField(default=timezone.now)
post = models.ForeignKey('Post', on_delete=models.CASCADE)
author = models.ForeignKey(User, on_delete=models.CASCADE)
class UserProfile(models.Model):
user = models.OneToOneField(User, primary_key=True, verbose_name='user', related_name='profile', on_delete=models.CASCADE)
name = models.CharField(max_length=30, blank=True, null=True)
bio = models.TextField(max_length=500, blank=True)
birth_date = models.DateField(null=True, blank=True)
location = models.CharField(max_length=100, blank=True, null=True)
picture = models.ImageField(upload_to='uploads/profile_pictures/', default='uploads/profile_pictures/default.png', blank=True)
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
if created:
UserProfile.objects.create(user=instance)
@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
instance.profile.save()

Updating the Profile

Now, let’s add the ability to update the profile from the user profile template. First we need to create a separate edit view. and then add a template to show the form. Finally we will add a url to access this page and then we will update our profile view to show the edit view only if the currently logged in user matches the user on the profile they are viewing.

social/views.py:

class ProfileEditView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
model = UserProfile
fields = ['name', 'bio', 'birth_date', 'location', 'picture']
template_name = 'social/profile_edit.html'

def get_success_url(self):
pk = self.kwargs['pk']
return reverse_lazy('profile', kwargs={'pk': pk})

def test_func(self):
profile = self.get_object()
return self.request.user == profile.user

social/templates/profile_edit.html:

{% extends 'landing/base.html' %}
{% load crispy_forms_tags %}
{% block content %}
<div class="container">
<div class="row mt-5">
<div class="col-md-3 col-sm-6">
<a href="{% url 'profile' object.pk %}" class="btn btn-light">Back to Your Profile</a>
</div>
</div>
<div class="row justify-content-center mt-5">
<div class="col-md-5 col-sm-12">
<h5>Update Your Profile</h5>
</div>
</div>
<div class="row justify-content-center mt-3 mb-5">
<div class="col-md-5 col-sm-12">
<form method="POST" enctype="multipart/form-data">
{% csrf_token %}
{{ form | crispy }}
<div class="d-grid gap-2">
<button class="btn btn-success mt-3">Update!</button>
</div>
</form>
</div>
</div>
</div>
{% endblock content %}

social/urls.py:

from django.urls import path
from .views import PostListView, PostDetailView, PostEditView, PostDeleteView, CommentDeleteView, ProfileView, ProfileEditView
urlpatterns = [
path('', PostListView.as_view(), name='post-list'),
path('post/<int:pk>/', PostDetailView.as_view(), name='post-detail'),
path('post/edit/<int:pk>/', PostEditView.as_view(), name='post-edit'),
path('post/delete/<int:pk>/', PostDeleteView.as_view(), name='post-delete'),
path('post/<int:post_pk>/comment/delete/<int:pk>/', CommentDeleteView.as_view(), name='comment-delete'),
path('profile/<int:pk>/', ProfileView.as_view(), name='profile'),
path('profile/edit/<int:pk>/', ProfileEditView.as_view(), name='profile-edit'),
]

social/templates/profile.html:

{% if profile.name %}
<h3 class="py-4">{{ profile.name }}
<span>
{% if request.user == user %}
<a href="{% url 'profile-edit' profile.pk %}" style="color: #333;"><i class="far fa-edit"></i></a>
{% endif %}
</span></h3>
{% endif %}

--

--