How do I extend the Django "login" form?

17,484

Solution 1

You need to subclass the AuthenticationForm class, and then you need to change your urls.py,

class MyAuthenticationForm(AuthenticationForm):
    # add your form widget here
    widget = .....

Then import this class into your urls.py file and update the call,

(r'^login/?$','django.contrib.auth.views.login',{'template_name':'login.html', 'authentication_form':MyAuthenticationForm}),

I'm too tired to lookup the links on the documentation site to see what type of field you need to use but this should do the trick to get you started without having to modify the django forms.py which you definitely should feel bad about changing!

Solution 2

Here's another solution using path instead of the deprecated url, to create a custom version of the page and form to reset your password after following a link sent out in an email. I'm using Django 2 and Python 3.

In my api app folder, I've created a folder templates/account with three files inside.

base.html:

<!DOCTYPE html>
<html>
  <head>
    <title>{% block head_title %}{% endblock %}</title>
    {% block extra_head %}
    {% endblock %}
  </head>
  <body>
    {% block body %}
    {% block content %}
    {% endblock %}
    {% endblock %}
    {% block extra_body %}
    {% endblock %}
  </body>
</html>

password_reset_complete.html:

{% extends "account/base.html" %}

{% block content %}

<h3>Password reset successful</h3>

<p>Your password has been changed. Please log in using the link below.</p>

<p><a href="/login/">Login</a></p>

{% endblock %}

password_reset_confirm.html:

{% extends 'account/base.html' %}

{% block content %}
  {% if validlink %}
    <h3>Enter your new password</h3>
    <form method="post">
      {% csrf_token %}
      {{ form.as_p }}
      <button type="submit">Change password</button>
    </form>
  {% else %}
    <p>
      The password reset link was invalid, possibly because it has already been used.
      Please request a new password reset.
    </p>
  {% endif %}
{% endblock %}

In my api app folder, I've created a file forms.py for my custom form which extends the default SetPasswordForm form.

forms.py:

from django import forms
from django.contrib.auth.forms import SetPasswordForm
from django.utils.translation import gettext, gettext_lazy as _
from django.contrib.auth import password_validation

class SetPasswordFormCustom(SetPasswordForm):
    new_password1 = forms.CharField(
        label=_("New password custom"),
        widget=forms.PasswordInput,
        strip=False,
    )
    new_password2 = forms.CharField(
        label=_("New password confirmation custom"),
        strip=False,
        widget=forms.PasswordInput,
        help_text=password_validation.password_validators_help_text_html(),
    )

And finally urls.py:

# api/urls.py
from django.urls import include, path
from django.contrib.auth import views
from django.conf.urls import include, url
from django.views.generic.base import RedirectView
from .forms import SetPasswordFormCustom

urlpatterns = [
...
path('reset/<uidb64>/<token>/',
    views.PasswordResetConfirmView.as_view(template_name='account/password_reset_confirm.html',
    form_class=SetPasswordFormCustom),
    name='password_reset_confirm'),
path('reset/done/', views.PasswordResetCompleteView.as_view(template_name='account/password_reset_complete.html'), name='password_reset_complete'),

]

Note 'from .forms import SetPasswordFormCustom'. The . allows a file in the same folder to be imported.

My reason for doing all this is that I haven't got reset password to work in my native React frontend, so I'm having to use the Django templates for this function. I want to be able to restyle it and have links that fit with my app as nearly as possible. But this method should generally allow you to create your own custom version of a Django auth form.

Solution 3

As milkypostman said, you need to make a subclass of auth.forms.AuthenticationForm.

Then, you can use django-crispy-forms for put the placeholders into the fields you want. It's as simple as this:

(app/forms.py)

class LoginWithPlaceholder(AuthenticationForm):

    def __init__(self, *args, **kwargs):
        super(LoginWithPlaceholder, self).__init__(*args, **kwargs)
        self.helper = FormHelper()
        self.helper.form_show_labels = False
        self.helper.layout = Layout(Div(Field('username', placeholder='username'), css_class="form-group"),
                                    Div(Field('password', placeholder='password'), css_class="form-group"),
                                    Div(Submit('submit', 'Log in')))

Finally, don't forget to use the crispy tag in your template:

<div class="col-sm-6 col-sm-offset-3">        
{% crispy form %}            
</div>
Share:
17,484
TIMEX
Author by

TIMEX

Updated on June 15, 2022

Comments

  • TIMEX
    TIMEX about 2 years

    So, right now I'm doing the basic login. In urls.py, I go to django contrib login:

    (r'^login/?$','django.contrib.auth.views.login',{'template_name':'login.html'}),
    

    That shoots it to here:

    @csrf_protect
    @never_cache
    def login(request, template_name='registration/login.html',
              redirect_field_name=REDIRECT_FIELD_NAME,
              authentication_form=AuthenticationForm):
    

    That view uses the AuthenticationForm forms model:

    class AuthenticationForm(forms.Form):
        """
        Base class for authenticating users. Extend this to get a form that accepts
        username/password logins.
        """
        username = forms.CharField(label=_("Username"), max_length=30)
        password = forms.CharField(label=_("Password"), widget=forms.PasswordInput)
    

    So...my objective is to to change the username form! By adding this to it: widget = forms.TextInput(attrs={'placeholder': 'username'}). That's it. That's all I want to add to the username input box. But, I don't want to change the actual django forms.py file, since that's part of django contrib and I feel bad changing that file.

    What do I do? Should I create a form that extends AuthenticationForm? If so, how do I import that? And how do I pass that in as an argument via my urls.py? I don't know what to do.