Building a Video Sharing Website: Part 3 Adding User Profiles

Video Tutorial

Code on Github

In this addition to the series we will build add user profiles. These profiles should be public and viewable by anyone logged in or not logged in. There should also be a way for the user that is connected to the profile to be able to edit the profile information. This button should only be availble if the user connected to the profile is logged in. Let’s get started building this.

Setting up a New App

First we need to start a new app and add it to installed_apps in settings.py. Just like we did in the beginning, we need to run this command:

python3 manage.py startapp profiles

And then we need to add it to installled_apps in settings.py:

INSTALLED_APPS = [
'videos',
'profiles',
'crispy_forms',
'django.contrib.sites', 'allauth',
'allauth.account',
'allauth.socialaccount',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]

Creating the Profile Model

Now that we have our new app set up, we can begin to build it. We will start with models.py and create the data structure for our profile. We need a couple fields here. We first want to connect our profile to a user, we also want to save a name, location, and image for the profile. Finally we will override the __str__ method to make the profile look a little better in the admin page.

from django.db import models
from django.contrib.auth.models import User
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
name = models.CharField(max_length=100, blank=True)
location = models.CharField(max_length=100, blank=True)
image = models.ImageField(upload_to='uploads/profile_pics', blank=True)
def __str__(self):
return f'{self.user.username} Profile'

Building the Profile View

Now let’s build a view to display a user profile. We will use the generic View class to inherit from. This class has different methods that will be called based on what HTTP method is used in the request. In this case, we will just be handling get requests to display the profile here. So, we will create a method called get that will hold all of the logic to be ran when we receive a get request. We will get a profile object using get_object_or_404 and we will get all of the videos uploaded by this user as well. We will pass both of these variables into the context dictionary to be passed into the template. Finally, we will render the template.

from django.shortcuts import render, get_object_or_404, reverse
from django.views import View
from .models import Profile
from videos.models import Video
class ProfileView(View): def get(self, request, pk, *args, **kwargs):
profile = get_object_or_404(Profile, pk=pk)
videos = Video.objects.all().filter(uploader=pk).order_by('-date_posted')
context = {
'profile': profile,
'videos': videos,
}
return render(request, 'profiles/profile.html', context)

Adding the Profile View URL Pattern

We will create the URL pattern for the profile view similar to how we created the DetailView for the videos. We will pass in a primary key to determine which user profile should be retrieved. We will also create a separate file and include it in our root urls.py file.

Root urls.py:

from django.contrib import admin
from django.urls import path, include
from videos import views as video_views
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
path('', video_views.Index.as_view(), name='index'),
path('videos/', include('videos.urls')),
path('profiles/', include('profiles.urls')),
path('accounts/', include('allauth.urls')),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Profiles urls.py:

from django.urls import path
from .views import ProfileView
urlpatterns = [
path('<int:pk>/', ProfileView.as_view(), name='profile'),
]

Building the Profile Template

In this template we want to first show the user, name, location, and image and then we want to loop through and display every video posted by the user. We will extend the base template found in the videos template folder, so all of our content will go in the same content block that is defined in that base template.

{% extends 'videos/base.html' %}{% block content %}
<div class="container">
<div class="row justify-content-center mt-5">
<div class="card col-md-6 col-sm-12 col-xs-12 d-flex flex-row align-items-center p-3 shadow">
<img class="rounded-circle mr-4" height="150" width="150" src="{{ profile.image.url }}">
<h1>{{ profile.user.username }}</h1>
</div>
</div>
<div class="row justify-content-center mt-5">
<div class="card col-md-6 col-sm-12 col-xs-12 p-3 shadow">
<h5>Name: {{ profile.name }}</h5>
<h5>Location: {{ profile.location }}</h5>
</div>
</div>
<div class="row justify-content-center my-5">
{% for video in videos %}
<div class="card col-md-3 col-sm-12 col-xs-12 p-3 shadow mr-md-3 my-3">
<img src="{{ video.thumbnail.url }}" class="card-img-top pt-2" alt="thumbnail">
<div class="card-body">
<h5 class="card-title text-center">{{ video.title }}</h5>
<p class="card-text text-muted text-center"><span class="mr-4">{{ video.uploader }}</span><span>{{ video.date_posted | date:"M d, Y" }}</span></p>
<a href="{% url 'video-detail' video.pk %}" class="stretched-link"></a>
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock %}

Adding the Profile Fields To the Sign Up Form

We want to allow the user to add profile information when they sign up. Since we are using alluth for authentication, it is pretty easy to do. But before we add it to the form, we need to create a ProfileForm:

profiles/forms.py:

from django import forms
from .models import Profile
class ProfileForm(forms.Form):
name = forms.CharField(max_length=30, label='Name')
location = forms.CharField(max_length=80, label='Location')
image = forms.ImageField(required=False)
def clean(self):
self.cleaned_data = super().clean()
image = self.cleaned_data.get('image')
if not image:
self.cleaned_data['image'] = 'uploads/profile_pics/default.jpg'
def signup(self, request, user):
user.save()
profile = Profile()
profile.user = user
profile.name = self.cleaned_data['name']
profile.location = self.cleaned_data['location']
profile.image = self.cleaned_data['image']
profile.save()

First we create a form and add the name, location, and image fields that we added to our model earlier. We then need to do two things, we need to set a default image if an image is not added to the form, and we need to save the user and then set the profile user, name, location, and image. Finally we save the profile as well. We set the image in the clean method and we need the signup method for allauth to run after we submit the form and save the data.

Now that we have that form created, we can very easily add it to the sign up form by adding one line to our settings.py:

# Additions to Sign Up Form
ACCOUNT_SIGNUP_FORM_CLASS = 'profiles.forms.ProfileForm'

Now when we load the sign up form, we should see the profile fields there as well.

Updating the Profile

Finally, we need to create a way for the logged in user to be able to update the profile information after it is created. We can use a regular UpdateView to easily accomplish this.

from django.shortcuts import render, get_object_or_404, reverse
from django.views import View
from django.views.generic.edit import UpdateView
from .models import Profile
from videos.models import Video
class ProfileView(View): def get(self, request, pk, *args, **kwargs):
profile = get_object_or_404(Profile, pk=pk)
videos = Video.objects.all().filter(uploader=pk).order_by('-date_posted')
context = {
'profile': profile,
'videos': videos,
}
return render(request, 'profiles/profile.html', context)class UpdateProfile(UpdateView):
model = Profile
fields = ['name', 'location', 'image']
template_name = 'profiles/update_profile.html'
def form_valid(self, form):
if not form.instance.image:
form.instance.image = 'uploads/profile_pics/default.jpg'
return super().form_valid(form)
def get_success_url(self):
return reverse('profile', kwargs={'pk': self.object.pk})

Now we can add to our profiles.urls.py:

from django.urls import path
from .views import ProfileView, UpdateProfile
urlpatterns = [
path('<int:pk>/', ProfileView.as_view(), name='profile'),
path('<int:pk>/update/', UpdateProfile.as_view(), name='update-profile'),
]

We can then create the template that the UpdateView renders:

{% extends 'videos/base.html' %}
{% load crispy_forms_tags %}
{% block content %}<div class="container mb-5">
<div class="row justify-content-center">
<div class="col-md-6 col-sm-12 col-xs-12">
<form method="POST" enctype="multipart/form-data">
{% csrf_token %}
<legend class="border-bottom mb-4">Update Profile</legend>
{{ form | crispy }}
<button class="btn btn-outline-info btn-block mt-3" type="submit">Submit</button>
</form>
</div>
</div>
</div>
{% endblock content %}

Finally, let’s add a link to the update template in our profiles template to show if the currently logged in user is the same as the profile user:

{% if request.user == profile.user %}
<div class="row justify-content-center mt-3">
<div class="col-md-6 col-sm-12">
<a href="{% url 'update-profile' profile.pk %}"><button class="btn btn-outline-info"><span><ion-icon class="mr-1" name="pencil-outline"></span>Edit Profile</button></a>
</div>
</div>
{% endif %}

That concludes this addition to this series of building a video sharing web application with Python and Django. We are starting to get a functional application together. In the future, we will continue to add more to this application.