I am trying to add message during the log-in process , for a user who has an account, but deactivated, that he must activate it if he wants to get in.
I use LoginView controller, that uses built-in standard form called AuthenticationForm
AuthenticationForm has a following method:
def confirm_login_allowed(self, user):
"""
Controls whether the given User may log in. This is a policy setting,
independent of end-user authentication. This default behavior is to
allow login by active users, and reject login by inactive users.
If the given user cannot log in, this method should raise a
``forms.ValidationError``.
If the given user may log in, this method should return None.
"""
if not user.is_active:
raise forms.ValidationError(
self.error_messages['inactive'],
code='inactive',
# and list of error messages within this class
error_messages = {
'invalid_login': _(
"Please enter a correct %(username)s and password. Note that both "
"fields may be case-sensitive."
),
'inactive': _("This account is inactive."),
}
So that technically if not user.is_active – it should show message 'inactive' but in my case for inactivated users with is_active = False DB table it shows the message 'invalid_login' instead. I am trying 100% correct login and password and user is not active but it shows me 'invalid_login' message. Then I just switch on is_active flag in DB to True and it lets me in easily. Do you have any idea why is that could be?
Final target is to show this message “'inactive': _("This account is inactive.")” to a user who has an account but deactivated. ( or custom message) Technically it should work but it doesn't. Thank you in advance and sorry in case you find this question elementary or dumb.
Tried:
class AuthCustomForm(AuthenticationForm):
def clean(self):
AuthenticationForm.clean(self)
user = ExtraUser.objects.get(username=self.cleaned_data.get('username'))
if not user.is_active and user:
messages.warning(self.request, 'Please Activate your account',
extra_tags="", fail_silently=True)
# return HttpResponseRedirect(' your url'))
FINALLY what helped:
class AuthCustomForm(AuthenticationForm):
def get_invalid_login_error(self):
user = ExtraUser.objects.get(username=self.cleaned_data.get('username'))
if not user.is_active and user:
raise forms.ValidationError(
self.error_messages['inactive'],
code='inactive',)
else:
return forms.ValidationError(
self.error_messages['invalid_login'],
code='invalid_login',
params={'username': self.username_field.verbose_name},
)
This is kind of wierd way to do it as DJANGO built -in code should have worked. I am not sure that i havent fixed my own mistake, made before here. perhaps i made things even worse.
This is a long answer but hopefully it will be useful and provide some insight as to how things are working behind the scenes.
To see why the 'inactive'
ValidationError
isn't being raised for an inactive user, we have to start by looking at how the LoginView
is implemented, specifically its post
method.
def post(self, request, *args, **kwargs):
"""
Handle POST requests: instantiate a form instance with the passed
POST variables and then check if it's valid.
"""
form = self.get_form()
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)
This method is called when the LoginView
receives the POST
request with the form data in it. get_form
populates the AuthenticationForm
with the POST
data from the request and then the form is checked, returning a different response depending on whether it's valid or not. We're concerned with the form checking, so let's look dig into what the is_valid
method is doing.
The Django docs do a good job of explaining how form and field validation works, so I won't go into too much detail. Basically, all that we need to know is that when the is_valid
method of a form is called, the form first validates all of its fields individually, then calls its clean
method to do any form-wide validation.
Here is where we need to look at how the AuthenticationForm
is implemented, as it defines its own clean
method.
def clean(self):
username = self.cleaned_data.get('username')
password = self.cleaned_data.get('password')
if username is not None and password:
self.user_cache = 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
This is where the confirm_login_allowed
method that you identified comes into play. We see that the username and password are passed to the authenticate
function. This checks the given credentials against all of the authentication backends defined by the AUTHENTICATION_BACKENDS
setting (see Django docs for more info), returning the authenticated user's User
model if successful and None
if not.
The result of authenticate
is then checked. If it's None
, then the user could not be authenticated, and the 'invalid_login'
ValidationError
is raised as expected. If not, then the user has been authenticated and confirm_login_allowed
raises the 'inactive'
ValidationError
if the user is inactive.
So why isn't the 'inactive'
ValidationError
raised?
It's because the inactive user fails to authenticate, and so authenticate
returns None
, which means get_invalid_login_error
is called instead of confirm_login_allowed
.
Why does the inactive user fail to authenticate?
To see this, i'm going to assume that you are not using a custom authentication backend, which means that your AUTHENTICATION_BACKENDS
setting is set to the default: ['django.contrib.auth.backends.ModelBackend']
. This means that ModelBackend
is the only authentication backend being used and we can look at its authenticate
method which is what the previously seen authenticate
function calls internally.
def authenticate(self, request, username=None, password=None, **kwargs):
if username is None:
username = kwargs.get(UserModel.USERNAME_FIELD)
if username is None or password is None:
return
try:
user = UserModel._default_manager.get_by_natural_key(username)
except UserModel.DoesNotExist:
# Run the default password hasher once to reduce the timing
# difference between an existing and a nonexistent user (#20760).
UserModel().set_password(password)
else:
if user.check_password(password) and self.user_can_authenticate(user):
return user
We're interested in the last if
statement.
if user.check_password(password) and self.user_can_authenticate(user):
return user
For our inactive user, we know that the password is correct, so check_password
will return True
. This means that it must be the user_can_authenticate
method which is returning False
and causing the inactive user to not be authenticated. Hold on, because we're almost there...
def user_can_authenticate(self, user):
"""
Reject users with is_active=False. Custom user models that don't have
that attribute are allowed.
"""
is_active = getattr(user, 'is_active', None)
return is_active or is_active is None
Aha! user_can_authenticate
returns False
if user.is_active
is False
which causes the user to not authenticate.
The solution
We can subclass ModelBackend
, override user_can_authenticate
, and point the AUTHENTICATION_BACKENDS
setting to this new subclass.
app/backends.py
from django.contrib.auth import backends
class CustomModelBackend(backends.ModelBackend):
def user_can_authenticate(self, user):
return True
settings.py
AUTHENTICATION_BACKENDS = [
'app.backends.CustomModelBackend',
]
I think this solution is cleaner than changing the logic of get_invalid_login_error
.
You can then override the 'inactive'
ValidationError
message by subclassing AuthenticationForm
, overriding error_messages
, and setting the authentication_form
attribute of the LoginView
to this new subclass.
from django.contrib.auth import forms as auth_forms, views as auth_views
from django.utils.translation import gettext_lazy as _
class CustomAuthenticationForm(auth_forms.AuthenticationForm):
error_messages = {
'invalid_login': _(
"Please enter a correct %(username)s and password. Note that both "
"fields may be case-sensitive."
),
'inactive': _("CUSTOM INACTIVE MESSAGE."),
}
class LoginView(auth_views.LoginView):
authentication_form = CustomAuthenticationForm