No Time Dad

A blog about web development written by a busy dad

Customizing Django's Built-in Login Form

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.