In this final part of the series, we are going to create and display a form where users can update their profile. This is what the profile page will look like by the end of this tutorial.
To create the forms, we are going to use ModelForm
. ModelForm
allows us to create forms that interact with a specific model inside the database.
forms.py
from django import forms
from django.contrib.auth.models import User
from .models import Profile
class UpdateUserForm(forms.ModelForm):
username = forms.CharField(max_length=100,
required=True,
widget=forms.TextInput(attrs={'class': 'form-control'}))
email = forms.EmailField(required=True,
widget=forms.TextInput(attrs={'class': 'form-control'}))
class Meta:
model = User
fields = ['username', 'email']
class UpdateProfileForm(forms.ModelForm):
avatar = forms.ImageField(widget=forms.FileInput(attrs={'class': 'form-control-file'}))
bio = forms.CharField(widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 5}))
class Meta:
model = Profile
fields = ['avatar', 'bio']
-
UpdateUserForm
interacts with the user model to let users update their username and email. -
UpdateProfileForm
interacts with the profile model to let users update their profile. - We gave the fields some bootstrap as well.
Now let's update the view to add the forms we just created.
views.py
from django.shortcuts import render, redirect
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from .forms import UpdateUserForm, UpdateProfileForm
@login_required
def profile(request):
if request.method == 'POST':
user_form = UpdateUserForm(request.POST, instance=request.user)
profile_form = UpdateProfileForm(request.POST, request.FILES, instance=request.user.profile)
if user_form.is_valid() and profile_form.is_valid():
user_form.save()
profile_form.save()
messages.success(request, 'Your profile is updated successfully')
return redirect(to='users-profile')
else:
user_form = UpdateUserForm(instance=request.user)
profile_form = UpdateProfileForm(instance=request.user.profile)
return render(request, 'users/profile.html', {'user_form': user_form, 'profile_form': profile_form})
- You should be familiar with most of the things we did in the above code but to recap, basically what it does is import the required forms and create instances of those forms depending on whether the request is get or post.
- If the form is submitted (request is post), we need to pass in the post data to the forms. But for the profile form there is a file/image data coming in with the request. This file data is placed in
request.FILES
so we need to pass in that too. - Then it populates the form fields with the current information of the logged in user i.e. The user form expects an instance of a user since it's working with the User model so we say
instance=request.user
while for the profile form we pass in an instance of the profile model by sayinginstance=request.user.profile
.
Finally let's update the template to display the forms
profile.html
{% extends "users/base.html" %}
{% block title %}Profile Page{% endblock title %}
{% block content %}
<div class="row my-3 p-3">
<img class="rounded-circle account-img" src="{{ user.profile.avatar.url }} " style="cursor: pointer;"/>
</div>
{% if user_form.errors %}
<div class="alert alert-danger alert-dismissible" role="alert">
<div id="form_errors">
{% for key, value in user_form.errors.items %}
<strong>{{ value }}</strong>
{% endfor %}
</div>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
{% endif %}
<div class="form-content">
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="form-row">
<div class="col-md-5">
<div class="form-group">
<label class="small mb-1">Username:</label>
{{ user_form.username }}
<label class="small mb-1">Email:</label>
{{ user_form.email }}
</div>
<div class="form-group">
<a href="#">Change Password</a>
<hr>
<label class="small mb-1">Change Avatar:</label>
{{ profile_form.avatar }}
</div>
<label class="small mb-1">Bio:</label> {{ profile_form.bio }}
</div>
</div>
<br><br>
<button type="submit" class="btn btn-dark btn-lg">Save Changes</button>
<button type="reset" class="btn btn-dark btn-lg">Reset</button>
</form>
</div>
{% endblock content %}
- Notice the dead link for Change Password. We will update it later at the end of this tutorial when we add change password functionality into our app.
Resizing Images In Django
Having large images saved only to show a scaled/smaller version of it on the profile page might cause our app to run slow. We can mitigate this problem by using pillow
to resize the large image and override it with the resized/smaller image.
Now, in order to save this resized image we need to override the save()
method which is a method that exists for all models and it is used to save an instance of the model.
Why do we need to override this method?
It is because we need a customized saving behavior.
Open models.py and add the following inside the Profile
model.
models.py
from PIL import Image
# resizing images
def save(self, *args, **kwargs):
super().save()
img = Image.open(self.avatar.path)
if img.height > 100 or img.width > 100:
new_img = (100, 100)
img.thumbnail(new_img)
img.save(self.avatar.path)
What the above code does is:
- Save the uploaded picture
- Open the image and check if it has a dimension larger than 100 pixels. If it has, resize the image and save it in that same path it was originally saved (overriding the original large image). Therefore, when the app is running, the browser is no longer getting that large image to display to the user.
Change Password
Usually in the profile page, users should be able to change their password. This is going to be really simple because we have discussed how users can reset their password if they forget it, and the process is going to be pretty much similar.
Django has a view called PasswordChangeView
which allows users to change their password. Let's create our own view which will extend from this and override some of the class's attributes.
views.py
from django.urls import reverse_lazy
from django.contrib.auth.views import PasswordChangeView
from django.contrib.messages.views import SuccessMessageMixin
class ChangePasswordView(SuccessMessageMixin, PasswordChangeView):
template_name = 'users/change_password.html'
success_message = "Successfully Changed Your Password"
success_url = reverse_lazy('users-home')
- Since this is similar to what we did here when resetting password, am not going to explain it here, but feel free to ask if there is any confusion.
Now go to the project's urls.py and create the route for this view.
user_management/urls.py
from django.urls import path
from users.views import ChangePasswordView
urlpatterns = [
path('password-change/', ChangePasswordView.as_view(), name='password_change'),
]
Finally create the template for the view.
change_password.html
{% extends "users/base.html" %}
{% block content %}
<div class="form-content my-3 p-3">
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-5">
<div class="card shadow-lg border-0 rounded-lg mt-0 mb-3">
<div class="card-header justify-content-center">
<h3 class="font-weight-light my-4 text-center">Change Your Password</h3>
</div>
{% if form.errors %}
<div class="alert alert-danger alert-dismissible" role="alert">
<div id="form_errors">
{% for key, value in form.errors.items %}
<strong>{{ value }}</strong>
{% endfor %}
</div>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
{% endif %}
<div class="card-body">
<form method="POST">
{% csrf_token %}
<div class="form-row">
<div class="col-md-10 offset-md-1">
<div class="form-group">
<label class="small mb-1" for="id_old_password">Old Password</label>
<input type="password" name="old_password" autocomplete="new-password"
class="form-control" required id="id_old_password"
placeholder="Enter Old Password"/>
<span>
</span>
</div>
</div>
</div>
<div class="form-row">
<div class="col-md-10 offset-md-1">
<div class="form-group">
<label class="small mb-1" for="id_new_password1">New Password</label>
<input type="password" name="new_password1" autocomplete="new-password"
class="form-control" required id="id_new_password1"
placeholder="Enter New Password"/>
<span>
</span>
</div>
</div>
</div>
<div class="form-row">
<div class="col-md-10 offset-md-1">
<div class="form-group">
<label class="small mb-1" for="id_new_password2">New Password Confirmation</label>
<input type="password" name="new_password2" autocomplete="new-password"
required id="id_new_password2" class="form-control"
placeholder="Confirm New Password"/>
</div>
</div>
</div>
<div class="form-row">
<div class="col-md-10 offset-md-1">
<div class="form-group mt-0 mb-1">
<button type="submit" class="col-md-12 btn btn-dark" id="reset">Update Password</button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock content %}
- In the profile page update the link to Change Password like this
<a href="{% url 'password_change' %}">Change Password</a>
Alright,
- Start the development server and run the usual command
python manage.py runserver
in your terminal. - Go to localhost, play around with the profile page and admin panel.
That's it for the series. I'm glad I was able to write this here. I thank you all for your time and feedback!
You can find the finished app in github
Top comments (9)
Thanks for this! It was very useful and very clearly explained. I am studying django at the moment and this is one of the better tutorials I have come across
Thanks a lot. I am glad you found it helpful.
Whoever that is following this tutorial and gets,
"Exception Type: RelatedObjectDoesNotExist
Exception Value: User has no profile."
Error, add:
user_form = UpdateUserForm(request.POST, instance=request.user)
just after the
if request.method == 'POST':
in the profile(request) function in your views.py.
Are you sure that you have setup Signals? The signals will allow you to automatically create the profile when the user is created.
Yes I did, but it still required that line of code in the views,py to run, maybe versioning issues.
I had a similar problem as Orioha Chikezirim. this is how my profile view ultimately looks like:
I think the problem is that you have created the superuser, before the signal was created. So the superuser doesnt have a profil. If you reset the database and create a new superuser you should be fine.
Hi, Excellent tutorial, helped me a lot.
I have one issue and that is that when someone clicks Submit on the Forgot Password page, they do not see the success message, it just redirects to the home page.
Any idea why this would happen, my code appears to be the same as yours in the github.
It's okay I have solved the issue.