Override Django LoginView Error Message With Custom AuthenticationForm

2.8k Views Asked by At

I have a class-based view that subclasses LoginView.

from django.contrib.auth.views import LoginView

class CustomLoginView(LoginView):

def get_success_url(self):
    url = self.get_redirect_url()
    return url or reverse_lazy('knowledgebase:user_home', kwargs={
        'username':self.request.user.username,
    })

I want to override the error message if a user's email is not yet active because they have to click a link sent to their email address. The current default message looks like this:

override LoginView error message

Instead of saying:

Please enter a correct email address and password. Note that both fields may be case-sensitive.

I want to say something to the effect of:

Please confirm your email so you can log in.

I tried:

accounts/forms.py

from django.contrib.auth.forms import AuthenticationForm
from django.utils.translation import gettext as _

class PickyAuthenticationForm(AuthenticationForm):
    def confirm_login_allowed(self, user):
        if not user.is_active:
            raise forms.ValidationError(
                _("Please confirm your email so you can log in."),
                code='inactive',
            )

accounts/views.py

class CustomLoginView(LoginView): # 1. <--- note: this is a class-based view

    form_class = PickyAuthenticationForm # 2. <--- note: define form here?

    def get_success_url(self):
        url = self.get_redirect_url()
        return url or reverse_lazy('knowledgebase:user_home', kwargs={
            'username':self.request.user.username,
        })

The result is absolutely no effect when I try to log in with a user that does exist, but hasn't verified their email address yet.

AuthenticationForm docs.

3

There are 3 best solutions below

0
JPG On BEST ANSWER

Method - 1

Django uses ModelBackend as default AUTHENTICATION_BACKENDS and which does not authenticate the inactive users.

This is also stated in Authorization for inactive users sections,

An inactive user is one that has its is_active field set to False. The ModelBackend and RemoteUserBackend authentication backends prohibits these users from authenticating. If a custom user model doesn’t have an is_active field, all users will be allowed to authenticate.

So, set AllowAllUsersModelBackend as your AUTHENTICATION_BACKENDS in settings.py

# settings.py

AUTHENTICATION_BACKENDS = ['django.contrib.auth.backends.AllowAllUsersModelBackend']

How much does it affect my Django app?

It doesn't affect anything other than the authentication. If we look into the source code of AllowAllUsersModelBackend class we can see it just allowing the inactive users to authenticate.


Method - 2

Personally, I don't recommend this method since method-1 is the Django way of tackling this issue.

Override the clean(...) method of PickyAuthenticationForm class and call the AllowAllUsersModelBackend backend as,

from django.contrib.auth.backends import AllowAllUsersModelBackend


class PickyAuthenticationForm(AuthenticationForm):
    def clean(self):
        username = self.cleaned_data.get('username')
        password = self.cleaned_data.get('password')

        if username is not None and password:
            backend = AllowAllUsersModelBackend()
            self.user_cache = backend.authenticate(self.request, username=username, password=password)
            if self.user_cache is None:
                raise self.get_invalid_login_error()
            else:
                self.confirm_login_allowed(self.user_cache)

        return self.cleaned_data

    def confirm_login_allowed(self, user):
        if not user.is_active:
            raise forms.ValidationError(
                "Please confirm your email so you can log in.",
                code='inactive',
            )

Result Screenshot

Screenshot

0
Saiful Azad On
2
Jarad On

I'm not convinced setting up a custom backend is the solution when I simply want to override a message. I did a temporary fix by defining form_invalid. Yes it's hacky but for now, it'll do the trick. Doubt this will help anyone but it was interesting to discover form.errors. Maybe someone can build off this to solve their specific problem.

def form_invalid(self, form):
    """If the form is invalid, render the invalid form."""
    #TODO: This is EXTREMELY HACKY!
    if form.errors:
        email = form.cleaned_data.get('username')
        if User.objects.filter(email=email, username=None).exists():
            if len(form.errors['__all__']) == 1:
                form.errors['__all__'][0] = 'Please confirm your email to log in.'
    return self.render_to_response(self.get_context_data(form=form))