Customizing Django's Built-in Login Form
January 27, 2021
Intro
Django is a masterpiece and you’ll never convince me otherwise. However, I’ve recently discovered that the process for adding custom css classes to Django’s built-in views is not super clear and it is easy to make mistakes. I am going to focus on Django’s LoginView
in this post, but I think a similar approach can be applied to any of the built-in views.
Setup
To use Django’s LoginView
you’ll need to add a route to your app’s urls.py
and create an html template for the view to render. Typically, this is how these files would look:
# my_app/urls.py
from django.urls import path
from django.contrib.auth.views import LoginView
app_name = 'my_app'
urlpatterns = [
path('login/', LoginView.as_view(), name="login")
]
<!-- my_app/templates/registration/login.html -->
<form method="POST" action="{% url 'my_app:login' %}">
{% csrf_token %} {{form.as_p}}
<input type="submit" value="Login" />
</form>
That is super easy and almost everything you need to create the form. If you visit the url you’d see a simple login form that just works. Here is the username input html that Django creates for you:
<input
type="text"
name="username"
autofocus=""
autocapitalize="none"
autocomplete="username"
maxlength="150"
class="my-username-class"
required=""
id="id_username"
/>
Most of these attributes are derived from Django’s AuthenticationForm
(which is itself derived from the User
model). Both the form and the model are tightly coupled.
You probably don’t want to leave your form as vanialla html like this because although it will work just fine, it really isn’t much to look at. You probably want to add styling via a css class.
Adding CSS to the Input
Now clearly since the template contains {{form.as_p}}
, you can’t just drop in a class
attribute on the input element as you might be used to. If you have done any amount of frontend work, your first inclination might be to just remove the form.as_p
and re-implement the input with your custom class:
...
<input class="my-username-class" type="text" name="username" autofocus="" autocapitalize="none" autocomplete="username" maxlength="150" class="my-username-class" required="" id="id_username"></p>
<input class="my-password-class" type="password" name="password" autocomplete="current-password" class="my-password-class" required="" id="id_password"></p>
...
I can assure you that this is the wrong approach. The problem is that you have now decoupled the form and taken control of it away from Django. So if you made any changes your User model or AuthenticationForm
instance Django would not know anything about it and you would have to manually edit your html to apply the changes.
Letting Django stay in control of the form rendering might feel counter-intuitive since you’re ultimately writing python code to manage html, but in the long run it is a huge time savings to just let Django create the form based on the form and model classes.
A Better Approach
Subclass the AuthenticationForm
and update the field’s attributes. In your app’s forms.py
:
# my_app/forms.py
from django.contrib.auth.forms import AuthenticationForm
class CustomLoginForm(AuthenticationForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['username'].widget.attrs.update(
{'class': 'my-username-class'}
)
self.fields['password'].widget.attrs.update(
{'class': 'my-password-class'}
)
One important thing to note here is the use of the update
method. If you simply set the attrs
to a new dict
like self.fields['username'].widget.attrs = [{'class': 'my-username-class'}]
, you’ll erase all of the existing attributes that Django is trying to add.
We now need to tell Django that we now want to use the subclassed form instead of the default built-in form. In your app’s urls.py
we’re using LoginView
, which has a property authentication_form
. We’ll pass that into the as_view
method with our new subclassed form.
# my_app/urls.py
from django.urls import path
from django.contrib.auth.views import LoginView
from my_app.forms import CustomLoginForm
app_name = 'my_app'
urlpatterns = [
path('login/', LoginView.as_view(
authentication_form=CustomLoginForm),
name="login"
)
]
Now if we look at our username
input we see it contains class='my-username-class
:
<input
class="my-username-class"
type="text"
name="username"
autofocus=""
autocapitalize="none"
autocomplete="username"
maxlength="150"
required=""
id="id_username"
/>
No further changes to login.html
are needed and it should stay exactly the same:
<!-- my_app/templates/registration/login.html -->
<form method="POST" action="{% url 'my_app:login' %}">
{% csrf_token %} {{form.as_p}}
<input type="submit" value="Login" />
</form>
I know it feels weird to relenquish control of the template, but it will be a big time savings once you start modifying models and forms in the future.