Fix circular import with custom user models

Fixes #1280

- Use SimpleLazyObject to defer get_user_model() evaluation
- Prevents circular import when custom user models import from axes
- Add test coverage for lazy evaluation in test_conf.py
This commit is contained in:
rodrigo.nogueira 2025-12-29 11:23:19 -03:00 committed by Aleksi Häkli
parent 95a8043341
commit 6703b66f17
2 changed files with 53 additions and 1 deletions

View file

@ -1,5 +1,6 @@
from django.conf import settings
from django.contrib.auth import get_user_model
from django.utils.functional import SimpleLazyObject
from django.utils.translation import gettext_lazy as _
# disable plugin when set to False
@ -43,8 +44,14 @@ settings.AXES_ONLY_ADMIN_SITE = getattr(settings, "AXES_ONLY_ADMIN_SITE", False)
settings.AXES_ENABLE_ADMIN = getattr(settings, "AXES_ENABLE_ADMIN", True)
# use a specific username field to retrieve from login POST data
def _get_username_field_default():
return get_user_model().USERNAME_FIELD
settings.AXES_USERNAME_FORM_FIELD = getattr(
settings, "AXES_USERNAME_FORM_FIELD", get_user_model().USERNAME_FIELD
settings,
"AXES_USERNAME_FORM_FIELD",
SimpleLazyObject(_get_username_field_default),
)
# use a specific password field to retrieve from login POST data

45
tests/test_conf.py Normal file
View file

@ -0,0 +1,45 @@
from django.test import TestCase
from django.utils.functional import SimpleLazyObject
class ConfTestCase(TestCase):
def test_axes_username_form_field_uses_lazy_evaluation(self):
"""
Test that AXES_USERNAME_FORM_FIELD uses SimpleLazyObject for lazy evaluation.
This prevents circular import issues with custom user models (issue #1280).
"""
from axes.conf import settings
# Verify that AXES_USERNAME_FORM_FIELD is a SimpleLazyObject if not overridden
# This is only the case when the setting is not explicitly defined
username_field = settings.AXES_USERNAME_FORM_FIELD
# The actual type depends on whether AXES_USERNAME_FORM_FIELD was overridden
# If it's using the default, it should be a SimpleLazyObject
# If overridden in settings, it could be a plain string
# Either way, it should be usable as a string
# Force evaluation and verify it works
username_field_str = str(username_field)
# Should get the default USERNAME_FIELD from the user model
# For the test suite, this is "username"
self.assertIsInstance(username_field_str, str)
self.assertTrue(len(username_field_str) > 0)
def test_axes_username_form_field_evaluates_correctly(self):
"""
Test that when AXES_USERNAME_FORM_FIELD is accessed, it correctly
resolves to the user model's USERNAME_FIELD.
"""
from django.contrib.auth import get_user_model
from axes.conf import settings
# Get the expected value
expected_username_field = get_user_model().USERNAME_FIELD
# Get the actual value from axes settings
actual_username_field = str(settings.AXES_USERNAME_FORM_FIELD)
# They should match
self.assertEqual(actual_username_field, expected_username_field)