TypeError: '<' not supported between instances of 'CombinedExpression' and 'int' when trying to implement the post_save signal with django-axes
I'm trying to create a signal which sents an email informing about an access attempt, when a user fails to provide correct credentials when logging in. Access attempts are traced by the django-axes package. If the failure_limit
is reached, an another signal is used. Comparing both values determines if this signal is used at the moment.
The TypeError occurs in failures = instance.failures_since_start
.
from axes.models import AccessAttempt
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.core.mail import mail_admins
from django.template import loader
from tajnanorka.settings import AXES_FAILURE_LIMIT
@receiver(post_save, sender=AccessAttempt)
def login_attempt_email(sender, instance, **kwargs):
def _render_email_message(context):
template = loader.get_template("foo.html")
return template.render(context)
failures = instance.failures_since_start
failure_limit = int(AXES_FAILURE_LIMIT)
context = dict(
failures_since_start = failures,
)
subject = 'bar'
message = _render_email_message(context)
if failures < failure_limit:
mail_admins(
subject,
message,
fail_silently=False,
html_message = message
)
AccessAttempt model:
class AccessBase(models.Model):
user_agent = models.CharField(_("User Agent"), max_length=255, db_index=True)
ip_address = models.GenericIPAddressField(_("IP Address"), null=True, db_index=True)
username = models.CharField(_("Username"), max_length=255, null=True, db_index=True)
http_accept = models.CharField(_("HTTP Accept"), max_length=1025)
path_info = models.CharField(_("Path"), max_length=255)
attempt_time = models.DateTimeField(_("Attempt Time"), auto_now_add=True)
class Meta:
app_label = "axes"
abstract = True
ordering = ["-attempt_time"]
class AccessAttempt(AccessBase):
get_data = models.TextField(_("GET Data"))
post_data = models.TextField(_("POST Data"))
failures_since_start = models.PositiveIntegerField(_("Failed Logins"))
def __str__(self):
return f"Attempted Access: {self.attempt_time}"
class Meta:
verbose_name = _("access attempt")
verbose_name_plural = _("access attempts")
unique_together = [["username", "ip_address", "user_agent"]]
Here's the whole traceback:
Traceback (most recent call last):
File "C:\Users\desp\anaconda3\envs\tajnanorka-venv\Lib\site-packages\django\core\handlers\exception.py", line 42, in inner
response = await get_response(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\desp\anaconda3\envs\tajnanorka-venv\Lib\site-packages\django\core\handlers\base.py", line 253, in _get_response_async
response = await wrapped_callback(
File "C:\Users\desp\anaconda3\envs\tajnanorka-venv\Lib\site-packages\django\views\generic\base.py", line 104, in view
return self.dispatch(request, *args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\desp\anaconda3\envs\tajnanorka-venv\Lib\site-packages\django\utils\decorators.py", line 48, in _wrapper
return bound_method(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\desp\anaconda3\envs\tajnanorka-venv\Lib\site-packages\django\views\decorators\debug.py", line 143, in sensitive_post_parameters_wrapper
return view(request, *args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\desp\anaconda3\envs\tajnanorka-venv\Lib\site-packages\django\utils\decorators.py", line 48, in _wrapper
return bound_method(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\desp\anaconda3\envs\tajnanorka-venv\Lib\site-packages\django\utils\decorators.py", line 188, in _view_wrapper
result = _process_exception(request, e)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\desp\anaconda3\envs\tajnanorka-venv\Lib\site-packages\django\utils\decorators.py", line 186, in _view_wrapper
response = view_func(request, *args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\desp\anaconda3\envs\tajnanorka-venv\Lib\site-packages\django\utils\decorators.py", line 48, in _wrapper
return bound_method(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\desp\anaconda3\envs\tajnanorka-venv\Lib\site-packages\django\views\decorators\cache.py", line 80, in _view_wrapper
response = view_func(request, *args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\desp\anaconda3\envs\tajnanorka-venv\Lib\site-packages\django\contrib\auth\views.py", line 88, in dispatch
return super().dispatch(request, *args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\desp\anaconda3\envs\tajnanorka-venv\Lib\site-packages\django\views\generic\base.py", line 143, in dispatch
return handler(request, *args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\desp\anaconda3\envs\tajnanorka-venv\Lib\site-packages\django\views\generic\edit.py", line 150, in post
if form.is_valid():
^^^^^^^^^^^^^^^
File "C:\Users\desp\anaconda3\envs\tajnanorka-venv\Lib\site-packages\django\forms\forms.py", line 197, in is_valid
return self.is_bound and not self.errors
^^^^^^^^^^^
File "C:\Users\desp\anaconda3\envs\tajnanorka-venv\Lib\site-packages\django\forms\forms.py", line 192, in errors
self.full_clean()
^^^^^^^^^^^^^^^^^
File "C:\Users\desp\anaconda3\envs\tajnanorka-venv\Lib\site-packages\django\forms\forms.py", line 328, in full_clean
self._clean_form()
^^^^^^^^^^^^^^^^^^
File "C:\Users\desp\anaconda3\envs\tajnanorka-venv\Lib\site-packages\django\forms\forms.py", line 349, in _clean_form
cleaned_data = self.clean()
^^^^^^^^^^^^
File "C:\Users\desp\anaconda3\envs\tajnanorka-venv\Lib\site-packages\django_otp\forms.py", line 272, in clean
self.cleaned_data = super().clean()
^^^^^^^^^^^^^^^
File "C:\Users\desp\anaconda3\envs\tajnanorka-venv\Lib\site-packages\django\contrib\auth\forms.py", line 250, in clean
self.user_cache = authenticate(
File "C:\Users\desp\anaconda3\envs\tajnanorka-venv\Lib\site-packages\django\views\decorators\debug.py", line 75, in sensitive_variables_wrapper
return func(*func_args, **func_kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\desp\anaconda3\envs\tajnanorka-venv\Lib\site-packages\django\contrib\auth\__init__.py", line 91, in authenticate
user_login_failed.send(
^
File "C:\Users\desp\anaconda3\envs\tajnanorka-venv\Lib\site-packages\django\dispatch\dispatcher.py", line 189, in send
response = receiver(signal=self, sender=sender, **named)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\desp\anaconda3\envs\tajnanorka-venv\Lib\site-packages\axes\signals.py", line 28, in handle_user_login_failed
AxesProxyHandler.user_login_failed(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\desp\anaconda3\envs\tajnanorka-venv\Lib\site-packages\axes\helpers.py", line 614, in inner
return func(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\desp\anaconda3\envs\tajnanorka-venv\Lib\site-packages\axes\handlers\proxy.py", line 119, in user_login_failed
return cls.get_implementation().user_login_failed(
File "C:\Users\desp\anaconda3\envs\tajnanorka-venv\Lib\site-packages\axes\handlers\database.py", line 216, in user_login_failed
attempt.save()
^^^^^^^^^^^^^^
File "C:\Users\desp\anaconda3\envs\tajnanorka-venv\Lib\site-packages\django\db\models\base.py", line 822, in save
self.save_base(
^
File "C:\Users\desp\anaconda3\envs\tajnanorka-venv\Lib\site-packages\django\db\models\base.py", line 924, in save_base
post_save.send(
^
File "C:\Users\desp\anaconda3\envs\tajnanorka-venv\Lib\site-packages\django\dispatch\dispatcher.py", line 189, in send
response = receiver(signal=self, sender=sender, **named)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\desp\Documents\Django-projects\projekt-tajnanorka\norkamarzen\signals.py", line 108, in login_attempt_email
if failures < failure_limit:
^^^^^^^^^^^^^^^^^^^^^^^^
Exception Type: TypeError at /accounts/login/
Exception Value: '<' not supported between instances of 'CombinedExpression' and 'int'
I've tried to convert failures
into int()
, but I don't know what to do else. Is there any way to compare these two values?
This error arises because instance.failures_since_start
does not return an integer, but instead a CombinedExpression
.
To fix this, you need to make sure that failures_since_start
is evaluated as an integer before comparing it to failure_limit
.
Try the following:
failures = int(instance.failures_since_start)
failure_limit = int(AXES_FAILURE_LIMIT)
You can not use the instance
, or at least not directly, since it uses an F
expression [Django-doc] to update the counter.
Use .refresh_from_db(…)
[Django-doc] to reread the counter from the database:
@receiver(post_save, sender=AccessAttempt)
def login_attempt_email(sender, instance, **kwargs):
def _render_email_message(context):
template = loader.get_template('foo.html')
return template.render(context)
instance.refresh_from_db()
failures = instance.failures_since_start
failure_limit = int(AXES_FAILURE_LIMIT)
context = dict(
failures_since_start=failures,
)
subject = 'bar'
message = _render_email_message(context)
if failures < failure_limit:
mail_admins(subject, message, fail_silently=False, html_message=message)