I'm using Django 1.10 and Celery 4.1
I have a shared_task which sends an email to the user.
# myapp/tasks.py
#shared_task
def notify_user(user_id):
# TODO: send email and do other stuff here
user = get_object_or_404(User, pk=user_id)
send_mail(
'Subject',
'Body',
'from#example.com',
[user.email],
)
I have another file which contains a function that calls puts that tasks into the queue.
# myapp/utils.py
# ...
def update_queue(self):
# increment no_of_used_referrals by 1
if no_of_used_referrals == 5:
notify_user.apply_async((self.user_id,))
else:
notify_user.apply_async((self.user_id,), eta=new_eta)
Now I am trying to test whether calling update_queue() (where all required checks passes) sends an email to the user when its executed.
I tried to do the following:
# myapp/tests.py
def update_queue_should_call_notify_user_immediately_after_five_referrals_were_used(self):
with unittest.mock.patch('myapp.tasks.notify_user.apply_async') as notify_user_mock:
# ...
for _ in range(5):
entry.update_queue()
self.assertTrue(notify_user_mock.called)
notify_user_mock.assert_called_with((user_id,))
# TODO: check if email was sent
# I tried using :
# self.assertEqual(len(mail.outbox), 1)
# but it fails with error saying 0 != 1
def test_notify_user_should_send_an_email(self):
notify_user.apply_async((user_id,))
# I tried using:
# self.assertEqual(len(mail.outbox), 1)
# but it fails with error saying 0 != 1
I have set EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend' in my project settings.
Can someone please tell me what is wrong with what I am doing and how to correctly test this case?
EDIT
I have updated my code where I excluded mocking - as suggested by #DanielRoseman.
EDIT2
Please see updated files above.
I am simulating referral system. Once 5 referral links associated with a particular user have been used, user get's some nice feature to their profile. Otherwise they have to wait for a specific time, which I set using eta argument on apply_async.
Every time I call update_queue I check if the number of referals is equal to 5(please see updated code above).
If it is - I want to call notify_user immediately, without passing eta argument value.
If it is not - I increment number of used referral links, revoke old notify_user task, create new notify_user task with new eta argument value.
In order to test that I am simulating that behaviour in for-loop, and I want to test whether after 5 iterations(equal to 5 used referral links) an email was sent to the user (for test purposes I use in-memory backend for email).
I put it here for someone that will face the same problem.
I've solved it with
TEST_RUNNER = 'djcelery.contrib.test_runner.CeleryTestSuiteRunner'
https://stackoverflow.com/a/46531472/7396169
I think this solution is suitable for unit testing.
tasks.py
from django.core.mail import EmailMessage
from django.template.loader import render_to_string
from django.contrib.auth import get_user_model
from accounts.models import Token
from celery import shared_task
#shared_task(bind=True)
def send_login_email_task(self, email):
try:
uid = str(uuid.uuid4())
Token.objects.create(email=email, uid=uid)
current_site = 'localhost:8000'
mail_subject = 'Activate your account.'
message = render_to_string('accounts/login_activation_email.html', {
'domain': current_site,
'uid': uid
})
print('called')
email = EmailMessage(mail_subject, message, to=[email])
email.send()
except Token.DoesNotExist:
logging.warning(
"Tried to send activation email to non-existing user '%s'", email)
except smtplib.SMTPException as exc:
raise self.retry(exc=exc)
test_tasks.py
from django.test import TestCase
from unittest.mock import patch
from django.contrib.auth import get_user_model
from celery.exceptions import Retry
from proj.celery import App
import smtplib
import uuid
import accounts.tasks
from accounts.models import Token
#patch('accounts.tasks.EmailMessage')
def test_send_login_email_task(self, mock_email_message):
# call task
token = Token.objects.get(email=self.email, uid=self.uid)
print(token.email)
accounts.tasks.send_login_email_task.apply_async((token.email,))
self.assertEqual(mock_email_message.called, True)
# patch EmailMessage
print(mock_email_message.call_args)
args, kwargs = mock_email_message.call_args
subject = args[0]
self.assertEqual(subject, 'Activate your account.')
self.assertEqual(kwargs, {'to': ['ama#example.com']})
Related
I am using Django-Verify-Email 1.0.6 in order to verify my email address.
I verified my email using this function:
send_verification_email(request, form)
if the user verifies its email then it fine. The problem arises when the email link gets expired and the user needs to verify email by resending the email.
This package contain a function resend_verification_email(request, encoded_email, encoded_token)
the description of the function says to pass encoded_email and encoded_token which was generated previously.
I don't think whether our system has that encoded_token and encoded_email save in DB. Can anyone help me with how to solve this issue?
Well, I guess here goes my first ever answer on Stack Overflow.
After I found this post I continued trying to look for a solution, and found it here: https://github.com/foo290/Django-Verify-Email/issues/6#issuecomment-782689288
While the post is certainly revealing, simply copy pasting the code did not work. So here is what I did in detail.
Firstly you need to extend the class:
from django.template.loader import render_to_string
from django.core.mail import send_mail
from django.core.mail import BadHeaderError
from smtplib import SMTPException
from django.utils.html import strip_tags
class VerifyEmail(_VerifyEmail):
def send_verification_link(self, request, user):
try:
useremail = user.email
verification_url = self.token_manager.generate_link(request, user, useremail)
subject = self.settings.get("subject")
msg = render_to_string(
self.settings.get("html_message_template", raise_exception=True),
{"link": verification_url},
)
try:
send_mail(
subject,
strip_tags(msg),
from_email=self.settings.get("from_alias"),
recipient_list=[useremail],
html_message=msg,
)
return user
except (BadHeaderError, SMTPException):
# user.delete()
return False
except Exception as error:
# user.delete()
if self.settings.get("debug_settings"):
raise Exception(error)
As you can see the method requires a request object. That is actually essential, as it is used to generate the link. The quickest way to do this then is to actually create a view. The following sample is more or less the page I needed - I had to find a way to verify and activate all the users that are not activated yet (we were not activating them at all before this), so what the script does is cycle through all the User objects marked as inactive and send the verification email:
from django.views import View
from VerifyEmail import VerifyEmail
from django.http import HttpResponse, HttpResponseForbidden
class VerifyInactiveUsersSender(View):
def get(self, request):
if(request.user.is_staff or request.user.is_superuser):
inactive_users = User.objects.filter(is_active=False)
for u in inactive_users:
inactive_user = VerifyEmail().send_verification_link(request, u)
return HttpResponse("Go check")
else:
return HttpResponseForbidden()
Given this code sends an email to all the inactive accounts I needed to add the security check, if(request.user.is_staff or request.user.is_superuser):, but that is not needed at all for simply creating a form where the user can get a second email. Hope this helps anybody.
I am currently using django rest password reset urls for my password reset needs and I have it attributed to a URL:
url("^password-reset/", include("django_rest_passwordreset.urls", namespace="password_reset")),
However, I would like to call this endpoint in a save method of a model using the email attributed to the user model. Is there any way to do something like this?
You can't 'call an endpoint in a save method', but you can make use of Signals to achieve this.
send_password_reset_token is a receiver function that triggered when a new user is created and sends a password reset link to the newly registered user's email automatically.
from datetime import timedelta
from django.urls import reverse
from django.conf import settings
from django.utils import timezone
from django.dispatch import receiver
from django.core.mail import send_mail
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.utils.translation import ugettext_lazy as _
from rest_framework import exceptions
from django_rest_passwordreset.models import ResetPasswordToken, clear_expired, get_password_reset_token_expiry_time, \
get_password_reset_lookup_field
from django_rest_passwordreset.signals import reset_password_token_created
#receiver(post_save, sender=User)
def send_password_reset_token(sender, instance, created, *args, **kwargs):
if created:
email = instance.email
# before we continue, delete all existing expired tokens
password_reset_token_validation_time = get_password_reset_token_expiry_time()
# datetime.now minus expiry hours
now_minus_expiry_time = timezone.now() - timedelta(hours=password_reset_token_validation_time)
# delete all tokens where created_at < now - 24 hours
clear_expired(now_minus_expiry_time)
# find a user by email address (case insensitive search)
users = User.objects.filter(**{'{}__iexact'.format(get_password_reset_lookup_field()): email})
active_user_found = False
# iterate overall users and check if there is any user that is active
# also check whether the password can be changed (is useable), as there could be users that are not allowed
# to change their password (e.g., LDAP user)
for user in users:
if user.eligible_for_reset():
active_user_found = True
# No active user found, raise a validation error
# but not if DJANGO_REST_PASSWORDRESET_NO_INFORMATION_LEAKAGE == True
if not active_user_found and not getattr(settings, 'DJANGO_REST_PASSWORDRESET_NO_INFORMATION_LEAKAGE', False):
raise exceptions.ValidationError({
'email': [_(
"There is no active user associated with this e-mail address or the password can not be changed")],
})
# last but not least: iterate over all users that are active and can change their password
# and create a Reset Password Token and send a signal with the created token
for user in users:
if user.eligible_for_reset():
# define the token as none for now
token = None
# check if the user already has a token
if user.password_reset_tokens.all().count() > 0:
# yes, already has a token, re-use this token
token = user.password_reset_tokens.all()[0]
else:
# no token exists, generate a new token
token = ResetPasswordToken.objects.create(user=user)
# send an e-mail to the user
context = {
'current_user': token.user,
'username': token.user.username,
'email': token.user.email,
'reset_password_url': "{}?token={}".format(reverse('password_reset:reset-password-request'), token.key)
}
send_mail(
'Subject here',
f'Your password_reset link is {context["reset_password_url"]}',
'from#example.com',
['to#example.com'],
fail_silently=False,
)
ResetPasswordRequestToken generation
Django email documentation
You can make use of signal to send email:
considering a Django app named profiles(you need to find and replace all occurrence of profiles with your app name).
create new file profiles/signals.py:
from django.dispatch import receiver
from django_rest_passwordreset.signals import reset_password_token_created
from django.urls import reverse
#receiver(reset_password_token_created)
def send_token_email(sender, instance, reset_password_token, *args, **kwargs):
reset_password_url = f"{instance.request.build_absolute_uri(reverse('password_reset:reset-password-confirm'))}"
message = f"Goto <a href='{reset_password_url}'>{reset_password_url}</a> and enter new password and token({reset_password_token.key})"
# send an e-mail to the user
from django.core.mail import send_mail
send_mail('Password reset',message,'noreply#somehost.com', [reset_password_token.user.email])
update profiles/app.py add ready method:
# ...
class ProfilesConfig(AppConfig):
# ...
def ready(self):
import profiles.signals
I understand that you want to send user an email after you add a user from admin.
You can do that by adding a save_model function in the admin. You can also send the password reset email using PasswordResetForm.
The full code cane be something like this:
from django.contrib import admin
from django.contrib.auth.forms import PasswordResetForm
class UserAdmin(admin.ModelAdmin):
def save_model(self, request, obj, form, change):
super().save_model(request, obj, form, change)
form = PasswordResetForm(data={"email": obj.email})
# calling save will send the email
form.save()
admin.site.register(User, UserAdmin)
Documentation for PasswordResetForm: https://docs.djangoproject.com/en/3.2/topics/auth/default/#django.contrib.auth.forms.PasswordResetForm
All you need do is send an email to them with a token and require that token from them.
if you use a save method within your model, that method will be called every time there is a saved change to an entry in that model which may not be what you want.
I need to implement auditlog feature in one of my project which is using Django 1.8 and Django-Rest-Framework 3.2.2. I have extended BaseUserManager class to create user model since I had to use email as a username in my application ( if this information matters ).
Below is my db design which will hold logs :
**fields type desc**
id pk ( auto_increment)
cust_id FK customer
customer_name FK customer
user_id FK user
user_name FK user
module Varchar(100) sales,order,billing,etc
action Varchar(10) Create/Update/Delete
previous_value varchar(500)
current_value varchar(500)
Datetime Datetime timestamp of change
I have tried https://pypi.python.org/pypi/django-audit-log but it has 2 issues as per my requirement-
It does not capture data as per my requirement which I understand is my issue and so I modified it's code and added my fields into it's model.
It is not capturing module information. Behaviour is random.
I am seeking advice to proceed with this feature. Which package would be best suitable for my task.
P.S I have also tried Django-reversion and I have no requirement of data versioning.
Thanks
I achieved what I needed by modifying auditlog code -
Added required field in LogEntry model of auditlog.
Modified log_create,log_update,log_delete functions of receivers.py to save information in newly added fields.
Using this I am halfway done. Now only issue I am facing is that since model instance of 1 table contains information of other tables as well due to FKs used in the table.
To solve this I could come up with a solution which works well but I am not satisfied with it.
I added a function like include_in_model() in each model and modified auditlog's registry.py register() function to get those fields and only use that to save information in LogEntry model.
This approach will require me to create this include_in_model() function in each of my model class and pass required fields for particular model. This way I am avoiding FK related information.
Django Simple History is an excellent app that I've used in production projects in the past, it will give you per model Audits against your users.
Furthermore, you should create your own Authentication Class which will be responsible for logging requests. Let's assume that a User uses a Token to authenticate with your API. It gets sent in the header of each HTTP Request to your API like so: Authorization: Bearer <My Token>. We should then log the User associated with the request, the time, the user's IP and the body.
This is pretty easy:
settings.py
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'common.authentication.MyTokenAuthenticationClass'
),
...
}
common/authentication.py
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from ipware.ip import get_real_ip
from rest_framework import authentication
from rest_framework import exceptions
from accounts.models import Token, AuditLog
class MyTokenAuthenticationClass(authentication.BaseAuthentication):
def authenticate(self, request):
# Grab the Athorization Header from the HTTP Request
auth = authentication.get_authorization_header(request).split()
if not auth or auth[0].lower() != b'bearer':
return None
# Check that Token header is properly formatted and present, raise errors if not
if len(auth) == 1:
msg = _('Invalid token header. No credentials provided.')
raise exceptions.AuthenticationFailed(msg)
elif len(auth) > 2:
msg = _('Invalid token header. Credentials string should not contain spaces.')
raise exceptions.AuthenticationFailed(msg)
try:
token = Token.objects.get(token=auth[1])
# Using the `ipware.ip` module to get the real IP (if hosted on ElasticBeanstalk or Heroku)
token.last_ip = get_real_ip(request)
token.last_login = timezone.now()
token.save()
# Add the saved token instance to the request context
request.token = token
except Token.DoesNotExist:
raise exceptions.AuthenticationFailed('Invalid token.')
# At this point, insert the Log into your AuditLog table:
AuditLog.objects.create(
user_id=token.user,
request_payload=request.body,
# Additional fields
...
)
# Return the Authenticated User associated with the Token
return (token.user, token)
Another solution would be to use django auditlog and use a custom middleware which does not capture the 'request.user' directly but at the moment when it is needed, by this time DRF will have set the correct 'request.user' so that it is no longer missing the username in the audit logs.
Create a file named (for example) auditlog_middleware.py and include it in the MIDDLEWARE in your settings.py instead of the default auditlog middleware.
from __future__ import unicode_literals
import threading
import time
from django.conf import settings
from django.db.models.signals import pre_save
from django.utils.functional import curry
from django.apps import apps
from auditlog.models import LogEntry
from auditlog.compat import is_authenticated
# Use MiddlewareMixin when present (Django >= 1.10)
try:
from django.utils.deprecation import MiddlewareMixin
except ImportError:
MiddlewareMixin = object
threadlocal = threading.local()
class AuditlogMiddleware(MiddlewareMixin):
"""
Middleware to couple the request's user to log items. This is accomplished by currying the signal receiver with the
user from the request (or None if the user is not authenticated).
"""
def process_request(self, request):
"""
Gets the current user from the request and prepares and connects a signal receiver with the user already
attached to it.
"""
# Initialize thread local storage
threadlocal.auditlog = {
'signal_duid': (self.__class__, time.time()),
'remote_addr': request.META.get('REMOTE_ADDR'),
}
# In case of proxy, set 'original' address
if request.META.get('HTTP_X_FORWARDED_FOR'):
threadlocal.auditlog['remote_addr'] = request.META.get('HTTP_X_FORWARDED_FOR').split(',')[0]
# Connect signal for automatic logging
set_actor = curry(self.set_actor, request=request, signal_duid=threadlocal.auditlog['signal_duid'])
pre_save.connect(set_actor, sender=LogEntry, dispatch_uid=threadlocal.auditlog['signal_duid'], weak=False)
def process_response(self, request, response):
"""
Disconnects the signal receiver to prevent it from staying active.
"""
if hasattr(threadlocal, 'auditlog'):
pre_save.disconnect(sender=LogEntry, dispatch_uid=threadlocal.auditlog['signal_duid'])
return response
def process_exception(self, request, exception):
"""
Disconnects the signal receiver to prevent it from staying active in case of an exception.
"""
if hasattr(threadlocal, 'auditlog'):
pre_save.disconnect(sender=LogEntry, dispatch_uid=threadlocal.auditlog['signal_duid'])
return None
#staticmethod
def set_actor(request, sender, instance, signal_duid, **kwargs):
"""
Signal receiver with an extra, required 'user' kwarg. This method becomes a real (valid) signal receiver when
it is curried with the actor.
"""
if hasattr(threadlocal, 'auditlog'):
if not hasattr(request, 'user') or not is_authenticated(request.user):
return
if signal_duid != threadlocal.auditlog['signal_duid']:
return
try:
app_label, model_name = settings.AUTH_USER_MODEL.split('.')
auth_user_model = apps.get_model(app_label, model_name)
except ValueError:
auth_user_model = apps.get_model('auth', 'user')
if sender == LogEntry and isinstance(request.user, auth_user_model) and instance.actor is None:
instance.actor = request.user
instance.remote_addr = threadlocal.auditlog['remote_addr']
I know that this answer is coming very late, but here it goes
Because DRF authenticates on the View level NOT on the Middleware level, the user is not yet attached to the request when AuditlogMiddleware runs, resulting in AnonymousUser
You can attach the logic from AuditlogMiddleware after your authentication
This logic connects some signals
This solution befits:
You don't have to decorate every View with it
it doesn't assume anything about AuditlogMiddleware or audit_log implementation in general. so if the code changes, this should still work
It doesn't force or duplicate DRF authentication.
#token_authentication_wrapper.py
from auditlog.middleware import AuditlogMiddleware
from rest_framework.authentication import TokenAuthentication
class TokenAuthenticationWrapper(TokenAuthentication):
def authenticate(self, request):
user, token = super().authenticate(request)
request.user = user # necessary for preventing recursion
AuditlogMiddleware().process_request(request)
return user, token
inherit from your favorite Authentication service e.g. BasicAuthentication SessionAuthentication, TokenAuthentication, etc...
and in setting.py
'DEFAULT_AUTHENTICATION_CLASSES': [
'path.to.file.token_authentication_wrapper.TokenAuthenticationWrapper',
]
First of all you can user package: https://github.com/jcugat/django-custom-user, to solve Email as Username field.
Then you can try to focus development with: http://django-reversion.readthedocs.io/en/stable/
The answer by #hassaan-alansary would have been ideal, but unfortunately the Auditlog devs made significant changes since he posted his answer, and I couldn't figure out how to reconcile their changes with Hassaan's answer.
The solution I ended up finding is based on what was shared here. Instead of writing a new DRF authentication method which invokes the middleware to do the logging, it creates a mixin which needs to be added to each of the DRF views you want added to the audit log. The solution below is the modified version of the one I ended up using from the link above.
# mixins.py
import threading
import time
from functools import partial
from django.db.models.signals import pre_save
from auditlog.models import LogEntry
threadlocal = threading.local()
class DRFDjangoAuditModelMixin:
"""
Mixin to integrate django-auditlog with Django Rest Framework.
This is needed because DRF does not perform the authentication at middleware layer
instead it performs the authentication at View layer.
This mixin adds behavior to connect/disconnect the signals needed by django-auditlog to auto
log changes on models.
It assumes that AuditlogMiddleware is activated in settings.MIDDLEWARE_CLASSES
"""
#staticmethod
def _set_actor(user, sender, instance, signal_duid, **kwargs):
# This is a reimplementation of auditlog.context._set_actor.
# Unfortunately the original logic cannot be used, because
# there is a type mismatch between user and auth_user_model.
if signal_duid != threadlocal.auditlog["signal_duid"]:
return
if (
sender == LogEntry
#and isinstance(user, auth_user_model)
and instance.actor is None
):
instance.actor = user
instance.remote_addr = threadlocal.auditlog["remote_addr"]
def initial(self, request, *args, **kwargs):
"""Overwritten to use django-auditlog if needed."""
super().initial(request, *args, **kwargs)
remote_addr = AuditlogMiddleware._get_remote_addr(request)
actor = request.user
set_actor = partial(
self._set_actor,
user=actor,
signal_duid=threadlocal.auditlog["signal_duid"],
)
pre_save.connect(
set_actor,
sender=LogEntry,
dispatch_uid=threadlocal.auditlog["signal_duid"],
weak=False,
)
def finalize_response(self, request, response, *args, **kwargs):
"""Overwritten to cleanup django-auditlog if needed."""
response = super().finalize_response(request, response, *args, **kwargs)
if hasattr(threadlocal, 'auditlog'):
pre_save.disconnect(sender=LogEntry, dispatch_uid=threadlocal.auditlog['signal_duid'])
del threadlocal.auditlog
return response
You then need to add this mixin to each of your views:
# views.py
...
class CustomerViewSet(DRFDjangoAuditModelMixin, ModelViewSet):
queryset = Client.objects.all()
serializer = ClientSerializer
....
The down side of this implementation is that it isn't DRY on a couple of levels. Not only do you need to add the mixin to each DRF view, but it copies code from nearly all the logging behaviour of auditlog, particularly private methods. I therefore expect this solution to either need adjustment in the future, or for it to also become obsolete.
The solution above is based on this revision of auditlog.
I want to log when session hash verification fails. The logging code should be inserted inside this https://github.com/django/django/blob/master/django/contrib/auth/init.py#L183 if block.
I am trying to figure out what would be the best way to implement this. Currently it looks like I will need to override the whole django.contrib.auth.middleware.AuthenticationMiddleware.
Do you have any tips for me?
Why don't you copy get_user function and put the logger like you want to:
from django.contrib.auth import *
def your_get_user(request):
"""
Returns the user model instance associated with the given request session.
If no user is retrieved an instance of `AnonymousUser` is returned.
"""
from django.contrib.auth.models import User, AnonymousUser
user = None
try:
user_id = _get_user_session_key(request)
backend_path = request.session[BACKEND_SESSION_KEY]
except KeyError:
pass
else:
if backend_path in settings.AUTHENTICATION_BACKENDS:
backend = load_backend(backend_path)
user = backend.get_user(user_id)
# Verify the session
if ('django.contrib.auth.middleware.SessionAuthenticationMiddleware'
in settings.MIDDLEWARE_CLASSES and hasattr(user, 'get_session_auth_hash')):
session_hash = request.session.get(HASH_SESSION_KEY)
session_hash_verified = session_hash and constant_time_compare(
session_hash,
user.get_session_auth_hash()
)
if not session_hash_verified:
log = logging.getLogger("YourLog")
log.debug(session_hash)
request.session.flush()
user = None
return user or AnonymousUser()
And use this like you want to in your code
I would like to log the user IP address in my Django application, specifically for login, logout and failed login events. I'm using Django builtin functions as follows:
from django.contrib.auth.signals import user_logged_in, user_logged_out, user_login_failed
from ipware.ip import get_ip
import logging
logger = logging.getLogger(__name__)
def log_logged_in(sender, user, request, **kwargs):
logger.info("%s User %s successfully logged in" % (get_ip(request), user))
def log_logged_out(sender, user, request, **kwargs):
logger.info("%s User %s successfully logged out" % (get_ip(request), user))
def log_login_failed(sender, credentials, **kwargs):
logger.warning("%s Authentication failure for user %s" % ("...IP...", credentials['username']))
user_logged_in.connect(log_logged_in)
user_logged_out.connect(log_logged_out)
user_login_failed.connect(log_login_failed)
The issue is that I haven't found a way to get the IP for the user_login_failed signal since this function does not have the request in the parameters (https://docs.djangoproject.com/en/1.7/ref/contrib/auth/#module-django.contrib.auth.signals). The credentials parameter is a dictionary that only contains the username and password fields.
How could I get the IP address for this signal?
Many thanks in advance for your help.
Unfortunately user_login_failed singal don't pass request as argument.
Checkout django-axes — https://github.com/django-pci/django-axes/
It uses a custom view decorator to track failed logins.
https://github.com/django-pci/django-axes/blob/master/axes/decorators.py#L273
I just found in newer Django version (I am using 2.1) has updated this and now it includes the request object in the user_login_failed signal:
https://docs.djangoproject.com/en/2.1/ref/contrib/auth/#django.contrib.auth.signals.user_login_failed
You could override the login form and intercept it there.
It has the request at that stage.
import logging
from django.contrib.admin.forms import AdminAuthenticationForm
from django import forms
log = logging.getLogger(__name__)
class AuthenticationForm(AdminAuthenticationForm):
def clean(self):
# to cover more complex cases:
# http://stackoverflow.com/questions/4581789/how-do-i-get-user-ip-address-in-django
ip = request.META.get('REMOTE_ADDR')
try:
data = super(AuthenticationForm, self).clean()
except forms.ValidationError:
log.info('Login Failed (%s) from (%s)', self.cleaned_data.get('username'), ip)
raise
if bool(self.user_cache):
log.info('Login Success (%s) from (%s)', self.cleaned_data.get('username'), ip)
else:
log.info('Login Failed (%s) from (%s)', self.cleaned_data.get('username'), ip)
return data
To install it into the site you need to attach it to django.contrib.admin.site.login_form
I would suggest doing it in your App's ready() method like so:
from django.contrib.admin import site as admin_site
class Config(AppConfig):
...
def ready(self):
# Attach the logging hook to the login form
from .forms import AuthenticationForm
admin_site.login_form = AuthenticationForm