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)
Back to Top