Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
*.min.js
*.min.css

2 changes: 1 addition & 1 deletion docs/developer/admin-theme.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Make sure ``openwisp_utils.admin_theme`` is listed in ``INSTALLED_APPS``
"django.contrib.staticfiles",
"openwisp_utils.admin_theme", # <----- add this
# add when using autocomplete filter
"admin_auto_filters", # <----- add this
"dalf", # <----- add this (django-admin-list-filter)
"django.contrib.sites",
# admin
"django.contrib.admin",
Expand Down
29 changes: 16 additions & 13 deletions docs/developer/admin-utilities.rst
Original file line number Diff line number Diff line change
Expand Up @@ -185,37 +185,40 @@ following example:
---------------------------------------------------------

The ``admin_theme`` sub app of this package provides an auto complete
filter that uses the *django-autocomplete* widget to load filter data
filter that uses Django's native autocomplete widget to load filter data
asynchronously.

This filter is powered by `django-admin-list-filter (dalf)
<https://github.com/vigo/django-admin-list-filter>`_, a lightweight and
actively maintained library that uses Django's built-in admin autocomplete
infrastructure.

This filter can be helpful when the number of objects is too large to load
all at once which may cause the slow loading of the page.

.. code-block:: python

from django.contrib import admin
from dalf.admin import DALFModelAdmin
from openwisp_utils.admin_theme.filters import AutocompleteFilter
from my_app.models import MyModel, MyOtherModel


class MyAutoCompleteFilter(AutocompleteFilter):
field_name = "field"
parameter_name = "field_id"
title = _("My Field")


@admin.register(MyModel)
class MyModelAdmin(admin.ModelAdmin):
list_filter = [MyAutoCompleteFilter, ...]
class MyModelAdmin(DALFModelAdmin):
list_filter = [
("field", AutocompleteFilter),
# ... other filters
]


@admin.register(MyOtherModel)
class MyOtherModelAdmin(admin.ModelAdmin):
search_fields = ["id"]
# The related model must have search_fields defined
search_fields = ["name", "id"]

To customize or know more about it, please refer to the
`django-admin-autocomplete-filter documentation
<https://github.com/farhan0581/django-admin-autocomplete-filter#usage>`_.
For more details, see the `django-admin-list-filter documentation
<https://github.com/vigo/django-admin-list-filter>`_.

Customizing the Submit Row in OpenWISP Admin
--------------------------------------------
Expand Down
7 changes: 7 additions & 0 deletions docs/user/settings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -263,3 +263,10 @@ default ``'openwisp_utils.admin_theme.views.AutocompleteJsonView'``

Dotted path to the ``AutocompleteJsonView`` used by the
``openwisp_utils.admin_theme.filters.AutocompleteFilter``.

.. note::

With the migration to `django-admin-list-filter (dalf)
<https://github.com/vigo/django-admin-list-filter>`_, this setting is
deprecated as DALF uses Django's native admin autocomplete
infrastructure.
5 changes: 5 additions & 0 deletions openwisp_utils/admin_theme/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ def openwisp_info(self, request, *args, **kwargs):
self.metric_collection.manage_form(request, context)
return render(request, "admin/openwisp_info.html", context)

def autocomplete_view(self, request):
"""Override to use custom AutocompleteJsonView that supports reverse relations."""
autocomplete_view = import_string(app_settings.AUTOCOMPLETE_FILTER_VIEW)
return autocomplete_view.as_view(admin_site=self)(request)

def get_urls(self):
autocomplete_view = import_string(app_settings.AUTOCOMPLETE_FILTER_VIEW)
return [
Expand Down
84 changes: 62 additions & 22 deletions openwisp_utils/admin_theme/filters.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
from admin_auto_filters.filters import AutocompleteFilter as BaseAutocompleteFilter
from dalf.admin import DALFRelatedFieldAjax
from django.contrib import messages
from django.contrib.admin.filters import FieldListFilter, SimpleListFilter
from django.contrib.admin.utils import NotRelationField, get_model_from_relation
from django.core.exceptions import ImproperlyConfigured, ValidationError
from django.db.models.fields import CharField, UUIDField
from django.urls import reverse
from django.utils.translation import gettext_lazy as _


Expand All @@ -28,9 +27,9 @@ def choices(self, changelist):
yield all_choice

def value(self):
"""Returns the querystring for this filter
"""Return the querystring for this filter.

If no querystring was supllied, will return None.
If no querystring was supplied, will return None.
"""
return self.used_parameters.get(self.parameter_name)

Expand Down Expand Up @@ -92,29 +91,70 @@ def expected_parameters(self):
return [self.lookup_kwarg, self.lookup_kwarg_isnull]


class AutocompleteFilter(BaseAutocompleteFilter):
template = "admin/auto_filter.html"
widget_attrs = {
"data-dropdown-css-class": "ow2-autocomplete-dropdown",
"data-empty-label": "-",
}

class Media:
css = {
"screen": ("admin/css/ow-auto-filter.css",),
}
js = BaseAutocompleteFilter.Media.js + ("admin/js/ow-auto-filter.js",)
class AutocompleteFilter(DALFRelatedFieldAjax):
"""AutocompleteFilter for Django admin using DALF.

This filter provides autocomplete functionality for foreign key and
many-to-many relationships using Django's native admin autocomplete
infrastructure.

Usage:
.. code-block:: python

def get_autocomplete_url(self, request, model_admin):
return reverse("admin:ow-auto-filter")
class MyFilter(AutocompleteFilter):
title = "My Field"
field_name = "my_field"
parameter_name = "my_field__id"
"""

def __init__(self, *args, **kwargs):
template = "admin/auto_filter.html"

def __init__(self, field, request, params, model, model_admin, field_path):
try:
return super().__init__(*args, **kwargs)
except ValidationError:
None
super().__init__(field, request, params, model, model_admin, field_path)
except (ValidationError, ValueError) as e:
# If there's a validation error (e.g., invalid UUID), initialize without error
# but store the error to display later
self._init_error = e
# Initialize basic attributes manually to prevent AttributeError
self.field = field
self.field_path = field_path
self.title = getattr(field, "verbose_name", field_path)
self.used_parameters = {}
# Required for Django's filter protocol
try:
from django.contrib.admin.filters import FieldListFilter

# Call the grandparent's __init__ to set up basic filter infrastructure
FieldListFilter.__init__(
self, field, request, params, model, model_admin, field_path
)
except Exception:
pass
Comment on lines +125 to +133
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Avoid catching bare Exception and silently passing; also, redundant import.

Two issues:

  1. FieldListFilter is already imported at line 3, so the local import is unnecessary.
  2. Catching all Exception types and silently passing masks potential bugs and makes debugging difficult. If the grandparent init fails, that's a significant issue that should at least be logged.
Proposed fix
             # Required for Django's filter protocol
             try:
-                from django.contrib.admin.filters import FieldListFilter
-
                 # Call the grandparent's __init__ to set up basic filter infrastructure
                 FieldListFilter.__init__(
                     self, field, request, params, model, model_admin, field_path
                 )
-            except Exception:
-                pass
+            except (TypeError, AttributeError):
+                # Fallback init may fail if field/model_admin are incompatible;
+                # filter will still work with degraded functionality
+                pass
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try:
from django.contrib.admin.filters import FieldListFilter
# Call the grandparent's __init__ to set up basic filter infrastructure
FieldListFilter.__init__(
self, field, request, params, model, model_admin, field_path
)
except Exception:
pass
try:
# Call the grandparent's __init__ to set up basic filter infrastructure
FieldListFilter.__init__(
self, field, request, params, model, model_admin, field_path
)
except (TypeError, AttributeError):
# Fallback init may fail if field/model_admin are incompatible;
# filter will still work with degraded functionality
pass
🧰 Tools
🪛 Ruff (0.14.14)

[error] 131-132: try-except-pass detected, consider logging the exception

(S110)


[warning] 131-131: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
In `@openwisp_utils/admin_theme/filters.py` around lines 124 - 132, Remove the
redundant local import of FieldListFilter and stop silently swallowing all
exceptions around the grandparent initializer call; either remove the try/except
so errors propagate or catch the exception, log it, and re-raise. Specifically,
in openwisp_utils/admin_theme/filters.py replace the try/except around
FieldListFilter.__init__(self, field, request, params, model, model_admin,
field_path) with a direct call (no local import) or with an except Exception as
e that calls logger = logging.getLogger(__name__);
logger.exception("FieldListFilter grandparent __init__ failed: %s", e); raise,
so initialization failures are not hidden.


Comment on lines +125 to +134
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Catching a broad Exception here and silently ignoring it can leave the filter in a partially initialized state and also mask real programming errors or configuration issues unrelated to invalid user input. It would be safer to narrow this to the specific exceptions you expect (e.g., ImportError or TypeError) or at least log the error so that unexpected failures in FieldListFilter.__init__ are visible during debugging.

Suggested change
try:
from django.contrib.admin.filters import FieldListFilter
# Call the grandparent's __init__ to set up basic filter infrastructure
FieldListFilter.__init__(
self, field, request, params, model, model_admin, field_path
)
except Exception:
pass
# Call the grandparent's __init__ to set up basic filter infrastructure
FieldListFilter.__init__(
self, field, request, params, model, model_admin, field_path
)

Copilot uses AI. Check for mistakes.
def expected_parameters(self):
"""Return expected parameters for this filter."""
if hasattr(self, "_init_error"):
return []
return super().expected_parameters()

def choices(self, changelist):
"""Return choices for this filter."""
if hasattr(self, "_init_error"):
# Return empty choices if initialization failed
return []
return super().choices(changelist)

def queryset(self, request, queryset):
# If there was an initialization error, show it and return unfiltered queryset
if hasattr(self, "_init_error"):
if isinstance(self._init_error, ValidationError):
error_msg = " ".join(self._init_error.messages)
else:
error_msg = str(self._init_error)
messages.error(request, error_msg)
return queryset
Comment on lines +135 to +156
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's examine the file under review
echo "=== File: openwisp_utils/admin_theme/filters.py ==="
wc -l openwisp_utils/admin_theme/filters.py

Repository: openwisp/openwisp-utils

Length of output: 160


🏁 Script executed:

#!/bin/bash
# Read the file around the lines mentioned in the review
cat -n openwisp_utils/admin_theme/filters.py | head -200

Repository: openwisp/openwisp-utils

Length of output: 7416


🏁 Script executed:

#!/bin/bash
# Search for the class definition and understand the broader context
rg -n "class.*Filter" openwisp_utils/admin_theme/filters.py -A 5

Repository: openwisp/openwisp-utils

Length of output: 1157


🏁 Script executed:

#!/bin/bash
# Search for _init_error usage to understand when/how it's set
rg -n "_init_error" --type=py -B 3 -A 3

Repository: openwisp/openwisp-utils

Length of output: 2638


🏁 Script executed:

#!/bin/bash
# Look for super().expected_parameters() to understand the parent class
rg -n "expected_parameters" --type=py -B 5 -A 5

Repository: openwisp/openwisp-utils

Length of output: 2088


🏁 Script executed:

#!/bin/bash
# Search for DALFRelatedFieldAjax and parameter_name usage
rg -n "parameter_name" openwisp_utils/admin_theme/filters.py -B 2 -A 2

Repository: openwisp/openwisp-utils

Length of output: 1306


🏁 Script executed:

#!/bin/bash
# Search for tests related to AutocompleteFilter initialization errors
find . -name "*.py" -type f -exec grep -l "AutocompleteFilter" {} \; | grep -i test

Repository: openwisp/openwisp-utils

Length of output: 140


🏁 Script executed:

#!/bin/bash
# Search for test files related to filters
fd -n "test.*filter" --type f

Repository: openwisp/openwisp-utils

Length of output: 297


🏁 Script executed:

#!/bin/bash
# Check for any documentation or comments about Django admin ChangeList behavior
rg -n "ChangeList\|expected_parameters\|get_filters" --type=py | head -30

Repository: openwisp/openwisp-utils

Length of output: 49


🏁 Script executed:

#!/bin/bash
# Look for parameter_name assignment in the AutocompleteFilter or parent class
rg -n "self.parameter_name" openwisp_utils/admin_theme/filters.py -B 5 -A 5

Repository: openwisp/openwisp-utils

Length of output: 1654


🏁 Script executed:

#!/bin/bash
# Check the tests to see if there are any tests for AutocompleteFilter error handling
cat tests/test_project/admin.py | head -150

Repository: openwisp/openwisp-utils

Length of output: 2895


🏁 Script executed:

#!/bin/bash
# Search for test cases related to AutocompleteFilter
rg -n "AutocompleteFilter" tests/ -B 3 -A 10

Repository: openwisp/openwisp-utils

Length of output: 4557


🏁 Script executed:

#!/bin/bash
# Check if parameter_name is set anywhere in AutocompleteFilter
rg -n "class AutocompleteFilter\|parameter_name\|field_name" openwisp_utils/admin_theme/filters.py -B 2 -A 2

Repository: openwisp/openwisp-utils

Length of output: 49


🏁 Script executed:

#!/bin/bash
# Look for the parent class DALFRelatedFieldAjax usage and documentation
python3 << 'EOF'
# Try to understand what parameters DALFRelatedFieldAjax might expose
import subprocess
result = subprocess.run(['rg', 'from dalf', '--type=py'], capture_output=True, text=True)
print(result.stdout)
EOF

Repository: openwisp/openwisp-utils

Length of output: 49


🏁 Script executed:

#!/bin/bash
# Check Django admin FieldListFilter to understand expected_parameters behavior
# First, let's see what the parent class of DALFRelatedFieldAjax likely is
rg -n "DALFRelatedFieldAjax\|FieldListFilter" openwisp_utils/admin_theme/filters.py -B 1 -A 5

Repository: openwisp/openwisp-utils

Length of output: 49


🏁 Script executed:

#!/bin/bash
# Search for any issue or comment about this in the codebase
rg -n "400\|IncorrectLookupParameters\|expected_parameters" --type=py | head -20

Repository: openwisp/openwisp-utils

Length of output: 49


🏁 Script executed:

#!/bin/bash
# Check if parameter_name is a class attribute or set during initialization
rg -n "parameter_name\s*=" openwisp_utils/admin_theme/filters.py

Repository: openwisp/openwisp-utils

Length of output: 202


🏁 Script executed:

#!/bin/bash
# Look for DALF documentation or usage
find . -name "*.py" -type f -exec grep -l "dalf" {} \; | head -5

Repository: openwisp/openwisp-utils

Length of output: 177


🏁 Script executed:

#!/bin/bash
# Check requirements/dependencies to understand DALF version
find . -name "requirements*.txt" -o -name "setup.py" -o -name "pyproject.toml" | xargs cat 2>/dev/null | grep -i dalf

Repository: openwisp/openwisp-utils

Length of output: 96


🏁 Script executed:

#!/bin/bash
# Look for any tests that demonstrate the initialization error behavior
rg -n "_init_error\|ValidationError\|initialization" tests/test_project/tests/ --type=py -B 3 -A 3

Repository: openwisp/openwisp-utils

Length of output: 49


🏁 Script executed:

#!/bin/bash
# Check the actual test for AutocompleteFilter to understand the filter behavior
sed -n '633,750p' tests/test_project/tests/test_selenium.py

Repository: openwisp/openwisp-utils

Length of output: 5157


🏁 Script executed:

#!/bin/bash
# Search for parameter_name initialization in DALFRelatedFieldAjax
# First, let's check if there's any documentation or source about DALF
python3 << 'EOF'
try:
    from dalf.admin import DALFRelatedFieldAjax
    import inspect
    
    # Get the source of the __init__ method
    source = inspect.getsource(DALFRelatedFieldAjax.__init__)
    print("=== DALFRelatedFieldAjax.__init__ ===")
    print(source[:1500])
    print("\n=== expected_parameters method ===")
    if hasattr(DALFRelatedFieldAjax, 'expected_parameters'):
        source = inspect.getsource(DALFRelatedFieldAjax.expected_parameters)
        print(source)
    else:
        print("No expected_parameters method found in DALFRelatedFieldAjax")
    
    # Check class attributes
    print("\n=== Class attributes ===")
    for attr in ['parameter_name', 'field_name']:
        if hasattr(DALFRelatedFieldAjax, attr):
            print(f"{attr}: {getattr(DALFRelatedFieldAjax, attr, 'NOT SET')}")
except Exception as e:
    print(f"Error: {e}")
EOF

Repository: openwisp/openwisp-utils

Length of output: 96


🏁 Script executed:

#!/bin/bash
# Look for any existing issues or comments about this in the repo
rg -n "400\|IncorrectLookupParameters\|unexpected.*parameter" --type=py -B 2 -A 2

Repository: openwisp/openwisp-utils

Length of output: 49


🏁 Script executed:

#!/bin/bash
# Check if there are any integration tests that test the error handling
find tests -name "*.py" -type f -exec grep -l "shelf.*AutocompleteFilter\|book.*AutocompleteFilter" {} \;

Repository: openwisp/openwisp-utils

Length of output: 94


🏁 Script executed:

#!/bin/bash
# Let's check the models to understand the filter relationship
rg -n "class Shelf\|class Book\|owner\|books_type" tests/test_project/models.py -B 1 -A 3 | head -80

Repository: openwisp/openwisp-utils

Length of output: 49


🏁 Script executed:

#!/bin/bash
# Look at the models file to understand the structure
cat tests/test_project/models.py | head -150

Repository: openwisp/openwisp-utils

Length of output: 4322


🏁 Script executed:

#!/bin/bash
# Search for parameter_name in DALFRelatedFieldAjax context
rg -n "field_name\|parameter_name" tests/test_project/admin.py -B 2 -A 2

Repository: openwisp/openwisp-utils

Length of output: 49


🏁 Script executed:

#!/bin/bash
# Check how Django's FieldListFilter normally defines expected_parameters
# by looking at imports and usage
rg -n "from django.contrib.admin" openwisp_utils/admin_theme/filters.py

Repository: openwisp/openwisp-utils

Length of output: 303


🏁 Script executed:

#!/bin/bash
# Let's trace through what happens during filter initialization
# by checking if parameter_name is set in the error path
python3 << 'EOF'
# Analyze the code flow
code_analysis = """
Looking at AutocompleteFilter.__init__ (lines 112-133):
1. super().__init__() is called in try block (line 114)
2. If ValidationError/ValueError, catch it and set _init_error (line 118)
3. After exception, parameter_name is NOT explicitly set in the except block
4. FieldListFilter.__init__() is called in the except block (lines 129-131)

Looking at expected_parameters() (lines 135-139):
- If _init_error exists, returns [] (line 138)
- Otherwise returns super().expected_parameters() (line 139)

The problem is:
- DALFRelatedFieldAjax likely sets parameter_name during super().__init__()
- If that fails, parameter_name is never set
- expected_parameters() returns [] in error state
- Django admin's ChangeList.get_filters() uses expected_parameters() to validate URL params
- If a URL param is not in any filter's expected_parameters(), it triggers a 400 error

This happens BEFORE queryset() is called, so the error message on line 155 is never shown.
"""
print(code_analysis)
EOF

Repository: openwisp/openwisp-utils

Length of output: 1025


🏁 Script executed:

#!/bin/bash
# Search for how Django handles IncorrectLookupParameters
# and check if it's relevant to our case
rg -n "IncorrectLookupParameters\|get_filters" --type=py -B 2 -A 2

Repository: openwisp/openwisp-utils

Length of output: 49


🏁 Script executed:

#!/bin/bash
# Look for Django version to understand ChangeList behavior
cat setup.py | grep -i "django\|version"

Repository: openwisp/openwisp-utils

Length of output: 483


🏁 Script executed:

#!/bin/bash
# Let's look for test cases that might test the error scenario
fd -t f "test.*filter" tests/

Repository: openwisp/openwisp-utils

Length of output: 49


🏁 Script executed:

#!/bin/bash
# Check if there are any tests related to AutocompleteFilter initialization
rg -n "AutocompleteFilter\|_init_error" tests/ --type=py -B 3 -A 5

Repository: openwisp/openwisp-utils

Length of output: 49


🏁 Script executed:

#!/bin/bash
# Let's trace through Django's expected_parameters behavior by checking documentation or usage
# Look at FieldListFilter in the codebase to understand the parent behavior
rg -n "class.*FieldListFilter\|def expected_parameters" --type=py -B 2 -A 5 | head -50

Repository: openwisp/openwisp-utils

Length of output: 49


🏁 Script executed:

#!/bin/bash
# Search for any URL parameter handling or validation in the codebase
rg -n "get_filters\|ChangeList\|expected_parameters" tests/ --type=py -B 2 -A 2

Repository: openwisp/openwisp-utils

Length of output: 49


🏁 Script executed:

#!/bin/bash
# Let's check if there are any comments or documentation about the Django behavior
# Look for 400 or Bad Request related code
rg -n "400\|BadRequest\|SuspiciousOperation" --type=py

Repository: openwisp/openwisp-utils

Length of output: 49


🏁 Script executed:

#!/bin/bash
# Create a small Python script to understand Django's filter parameter validation
python3 << 'EOF'
# Let's document what we know about Django's ChangeList.get_filters()
analysis = """
ANALYSIS OF THE ISSUE:

The review comment claims that:
1. When _init_error is set, expected_parameters() returns []
2. Django admin's ChangeList.get_filters() uses expected_parameters() to validate URL params
3. If a URL param (e.g., owner__id=invalid) is not claimed by any filter, Django raises 400
4. This happens BEFORE queryset() is called
5. Therefore, the error message in queryset() (line 155) is never shown

KEY EVIDENCE:
- AutocompleteFilter.__init__ catches ValidationError/ValueError at line 115
- This happens when super().__init__() fails (e.g., invalid UUID in URL)
- _init_error is stored (line 118)
- expected_parameters() returns [] when _init_error exists (line 138)
- queryset() tries to show error message (line 155)

THE POTENTIAL PROBLEM:
- If the URL contains a parameter like ?owner__id=invalid-uuid
- DALFRelatedFieldAjax.__init__() validates this and raises ValidationError
- The filter catches this and sets _init_error
- expected_parameters() returns [], not claiming the owner__id parameter
- Django admin's ChangeList sees owner__id as an unexpected parameter
- Django raises a 400 error BEFORE queryset() is called
- The user never sees the friendly error message

SOLUTION SUGGESTED:
- expected_parameters() should return the expected parameter names even in error state
- This prevents Django from rejecting the request
- queryset() can still show the friendly error message
- Need to derive the expected parameter name from field_path or parameter_name
"""
print(analysis)
EOF

Repository: openwisp/openwisp-utils

Length of output: 1558


🏁 Script executed:

#!/bin/bash
# Compare InputFilter's approach with AutocompleteFilter
echo "=== InputFilter.expected_parameters() ==="
sed -n '90,91p' openwisp_utils/admin_theme/filters.py
echo ""
echo "=== InputFilter.__init__ setup ==="
sed -n '77,79p' openwisp_utils/admin_theme/filters.py
echo ""
echo "=== AutocompleteFilter expected_parameters() ==="
sed -n '135,139p' openwisp_utils/admin_theme/filters.py

Repository: openwisp/openwisp-utils

Length of output: 640


🏁 Script executed:

#!/bin/bash
# Let's look for the complete setup.py to understand Django version requirements
cat setup.py | grep -A 20 "install_requires\|extras_require"

Repository: openwisp/openwisp-utils

Length of output: 1037


🏁 Script executed:

#!/bin/bash
# Search for any documentation or code showing how DALFRelatedFieldAjax sets parameter_name
rg -n "parameter_name\|field_name" openwisp_utils/admin_theme/filters.py -B 5 -A 5 | grep -A 10 "AutocompleteFilter"

Repository: openwisp/openwisp-utils

Length of output: 49


🏁 Script executed:

#!/bin/bash
# Let's check if there's a way to access the expected parameter even when init fails
# by looking at what attributes are available in the error state
cat -n openwisp_utils/admin_theme/filters.py | sed -n '112,133p'

Repository: openwisp/openwisp-utils

Length of output: 1302


🏁 Script executed:

#!/bin/bash
# Check if parameter_name is defined as a class attribute in AutocompleteFilter
rg -n "class AutocompleteFilter" openwisp_utils/admin_theme/filters.py -A 20

Repository: openwisp/openwisp-utils

Length of output: 855


🏁 Script executed:

#!/bin/bash
# Search for how the filter is registered in admin to understand parameter_name
rg -n "AutocompleteFilter\|parameter_name" tests/test_project/admin.py -B 2 -A 2

Repository: openwisp/openwisp-utils

Length of output: 49


🏁 Script executed:

#!/bin/bash
# Let's check the Django source code or documentation about how expected_parameters affects request processing
# First, let's search for any comments or documentation in the codebase about this behavior
rg -n "expected_parameters\|ChangeList\|get_filters" --type=py -B 3 -A 3 | head -100

Repository: openwisp/openwisp-utils

Length of output: 49


🏁 Script executed:

#!/bin/bash
# Let's create a test scenario to understand what Django expects from expected_parameters
python3 << 'EOF'
# Analyze the expected_parameters contract in Django admin filters
analysis = """
VERIFICATION OF THE REVIEW CLAIM:

From the code analysis:

1. CONFIRMED: expected_parameters() returns [] when _init_error is set
   - Line 137-138: if hasattr(self, "_init_error"): return []

2. CONFIRMED: In error state, field_path is available
   - Line 121: self.field_path = field_path
   - This could be used to derive the parameter name

3. CONFIRMED: The parent class (InputFilter) shows the pattern
   - InputFilter.expected_parameters() returns [self.lookup_kwarg, self.lookup_kwarg_isnull]
   - This shows filters are expected to claim their parameters

4. THE CLAIM ABOUT 400 ERROR:
   - Django admin's ChangeList.get_filters() validates URL parameters against expected_parameters()
   - If a URL parameter is not claimed by any filter, it's treated as an invalid lookup
   - This results in IncorrectLookupParameters exception → 400 response
   - This happens in ChangeList.__init__() → get_filters() BEFORE queryset() is called
   
   Django source shows:
   - ChangeList.__init__() calls get_filters() early
   - get_filters() uses expected_parameters() to check if a URL param should be processed
   - If a parameter is unexpected, IncorrectLookupParameters is raised
   - This is caught in ChangeList.__init__() and typically results in 400 Bad Request

5. THE FLOW ISSUE:
   - AutocompleteFilter.__init__() fails with ValidationError (e.g., invalid UUID)
   - Filter catches this, stores as _init_error
   - expected_parameters() returns []
   - User visits URL with the invalid parameter (e.g., ?owner__id=invalid-uuid)
   - Django's ChangeList.__init__() raises 400 BEFORE queryset() is called
   - queryset() (which would show friendly error) is never executed

THE PROPOSED FIX IS CORRECT:
- expected_parameters() should return the parameter names even in error state
- This prevents 400 error and allows queryset() to execute
- The parameter name can be derived from field_path

EXAMPLE:
For a filter on 'owner' field with parameter_name="owner__id__exact":
- Even in error state, expected_parameters() should return ["owner__id__exact"]
- This ensures the URL parameter is "expected" by this filter
- Django won't reject it, and queryset() can handle it gracefully
"""
print(analysis)
EOF

Repository: openwisp/openwisp-utils

Length of output: 2258


🏁 Script executed:

#!/bin/bash
# Check if there's a way to determine the expected parameter name in the error state
# by looking at what information is available
cat -n openwisp_utils/admin_theme/filters.py | sed -n '104,140p'

Repository: openwisp/openwisp-utils

Length of output: 1831


🏁 Script executed:

#!/bin/bash
# Check the usage pattern in admin.py to understand what parameter names are expected
cat tests/test_project/admin.py | grep -A 3 "AutocompleteFilter"

Repository: openwisp/openwisp-utils

Length of output: 362


🏁 Script executed:

#!/bin/bash
# Look at what super().expected_parameters() would return if successful
# by checking if there are any other clues in the codebase
rg -n "owner__id\|book__id\|shelf__id" tests/ --type=py -B 1 -A 1 | head -30

Repository: openwisp/openwisp-utils

Length of output: 49


🏁 Script executed:

#!/bin
# Let's verify the exact issue by checking the test selenium file
sed -n '700,730p' tests/test_project/tests/test_selenium.py

Repository: openwisp/openwisp-utils

Length of output: 1502


🏁 Script executed:

#!/bin/bash
# Let's check the test that verifies the owner__id parameter is used
grep -n "owner__id" tests/test_project/tests/test_selenium.py

Repository: openwisp/openwisp-utils

Length of output: 141


expected_parameters() returning [] causes Django to reject the request with 400 before queryset() executes, preventing the friendly error message.

When _init_error is set, expected_parameters() returns []. Django admin's ChangeList.__init__() validates all URL query parameters against the expected parameters returned by active filters. If a parameter (e.g., ?owner__id__exact=invalid-uuid) is not claimed by any filter, Django raises IncorrectLookupParameters and returns a 400 Bad Request — before queryset() is ever called.

This means messages.error(request, error_msg) on line 155 never executes, and the user sees a raw error page instead of a friendly message.

Modify expected_parameters() to return the correct parameter names even in the error state. This allows Django to accept the request, letting queryset() execute and display the friendly error message.

Proposed fix
     def expected_parameters(self):
         """Return expected parameters for this filter."""
         if hasattr(self, "_init_error"):
-            return []
+            # Still claim our parameters so Django doesn't reject the request
+            return [self.field_path + "__id__exact"]
         return super().expected_parameters()

Verify the exact parameter name format matches what the parent class (DALFRelatedFieldAjax) generates. You may need to store the expected parameter list before calling super().__init__() if the pattern is more complex or configurable via subclass attributes.

🤖 Prompt for AI Agents
In `@openwisp_utils/admin_theme/filters.py` around lines 135 - 156, When
_init_error is set, expected_parameters() must still return the real parameter
names so Django's ChangeList won't raise IncorrectLookupParameters; change
expected_parameters() to return the parent's expected parameter list (call and
return super().expected_parameters()) even when hasattr(self, "_init_error"); if
the parent list isn't available because initialization failed earlier,
capture/cache the expected parameter names before calling super().__init__ (or
derive them the same way DALFRelatedFieldAjax would) and return that cached list
from expected_parameters(); keep queryset() unchanged so messages.error(request,
error_msg) can run.


try:
return super().queryset(request, queryset)
except ValidationError as e:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use strict";
django.jQuery(document).ready(function () {
// unbinding default event handlers of admin_auto_filters
// unbinding default event handlers of autocomplete filters (DALF)
django.jQuery("#changelist-filter select, #grp-filters select").off("change");
django.jQuery("#changelist-filter select, #grp-filters select").off("clear");

Expand Down
23 changes: 20 additions & 3 deletions openwisp_utils/admin_theme/templates/admin/auto_filter.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
{% load i18n %}
<div class="ow-filter auto-filter">
{% include 'django-admin-autocomplete-filter/autocomplete-filter.html' %}
<div class="auto-filter-choices"></div>
<div class="ow-filter ow-autocomplete-filter">
{% with params=choices|last %}
<div class="filter-title">
<h3>{% blocktranslate with filter_title=title %} By {{ filter_title }} {% endblocktranslate %}</h3>
<input class="djal-selected-value" type="hidden" value="{{ params.selected_value|default_if_none:'' }}" />
<input class="djal-selected-text" type="hidden" value="{{ params.selected_text|default_if_none:'' }}" />
<select
class="django-admin-list-filter-ajax"
data-ajax-url="{{ params.ajax_url }}"
data-app-label="{{ params.app_label }}"
data-model-name="{{ params.model_name }}"
data-lookup-kwarg="{{ params.lookup_kwarg }}"
data-theme="admin-autocomplete"
data-field-name="{{ params.field_name }}"></select>
</div>
<div class="filter-options">
<!-- Hidden anchor for filter button to detect changes -->
<a name="{{ params.lookup_kwarg }}" parameter_name="{{ params.lookup_kwarg }}"></a>
</div>
{% endwith %}
</div>
4 changes: 2 additions & 2 deletions openwisp_utils/admin_theme/templatetags/ow_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ def ow_create_filter(cl, spec, total_filters):
choices = list(spec.choices(cl))
selected_choice = None
for choice in choices:
if choice["selected"]:
selected_choice = choice["display"]
if choice.get("selected", False):
selected_choice = choice.get("display", "")
return tpl.render(
{
"title": spec.title,
Expand Down
71 changes: 13 additions & 58 deletions openwisp_utils/admin_theme/views.py
Original file line number Diff line number Diff line change
@@ -1,62 +1,17 @@
from admin_auto_filters.views import AutocompleteJsonView as BaseAutocompleteJsonView
from django.core.exceptions import PermissionDenied
from django.http import JsonResponse
from django.contrib.admin.views.autocomplete import (
AutocompleteJsonView as DjangoAutocompleteJsonView,
)
from django.db.models.fields.reverse_related import ManyToOneRel


class AutocompleteJsonView(BaseAutocompleteJsonView):
admin_site = None
class AutocompleteJsonView(DjangoAutocompleteJsonView):

def get_empty_label(self):
return "-"

def get_allow_null(self):
return True

def get(self, request, *args, **kwargs):
(
self.term,
self.model_admin,
self.source_field,
_,
) = self.process_request(request)

if not self.has_perm(request):
raise PermissionDenied

self.support_reverse_relation()
self.object_list = self.get_queryset()
context = self.get_context_data()
# Add option for filtering objects with None field.
results = []
empty_label = self.get_empty_label()
if (
getattr(self.source_field, "null", False)
and self.get_allow_null()
and not getattr(self.source_field, "_get_limit_choices_to_mocked", False)
and not self.term
or self.term == empty_label
def get_queryset(self):
"""Override to support reverse relations without get_limit_choices_to()."""
# Handle reverse relations (ManyToOneRel) that don't have get_limit_choices_to
if isinstance(self.source_field, ManyToOneRel) or not hasattr(
self.source_field, "get_limit_choices_to"
):
# The select2 library requires data in a specific format
# https://select2.org/data-sources/formats.
# select2 does not render option with blank "id" (i.e. '').
# Therefore, "null" is used here for "id".
results += [{"id": "null", "text": empty_label}]
results += [
{"id": str(obj.pk), "text": self.display_text(obj)}
for obj in context["object_list"]
]
return JsonResponse(
{
"results": results,
"pagination": {"more": context["page_obj"].has_next()},
}
)

def support_reverse_relation(self):
if not hasattr(self.source_field, "get_limit_choices_to"):
self.source_field._get_limit_choices_to_mocked = True

def get_choices_mock():
return {}

self.source_field.get_limit_choices_to = get_choices_mock
# Mock the method for reverse relations
self.source_field.get_limit_choices_to = lambda: {}
return super().get_queryset()
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
install_requires=[
"django-model-utils>=4.5,<5.1",
"django-minify-compress-staticfiles~=1.1.0",
"django-admin-autocomplete-filter~=0.7.1",
"dalf>=0.7.0,<1.0.0",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

dalf 0.7.x django-admin-list-filter Django version compatibility requirements

💡 Result:

For dalf (django-admin-list-filter) 0.7.x:

  • Django: 5.0+ (the project explicitly says it “supports Django version 5 and above” and classifiers list Django 5.0, 5.1, 5.2) [1].
  • Python: >= 3.11 (PyPI metadata: “Requires: Python >=3.11”) [1].

Sources: PyPI project page / metadata [1] and the upstream GitHub README stating Django 5+ support [2].

[1] (pypi.org)
[2] (github.com)

Citations:


🏁 Script executed:

# Check the setup.py for Django version requirements in this repository
cat setup.py | head -50

Repository: openwisp/openwisp-utils

Length of output: 1791


🏁 Script executed:

# Also check for tox.ini or other configuration files that might specify Django versions
fd -e "tox.ini" -e "requirements*.txt" -e ".python-version" | head -20

Repository: openwisp/openwisp-utils

Length of output: 49


🏁 Script executed:

cat setup.py | tail -100

Repository: openwisp/openwisp-utils

Length of output: 2685


🏁 Script executed:

# Check for tox.ini or pytest configuration that shows Django version testing
cat tox.ini 2>/dev/null || echo "No tox.ini found"

Repository: openwisp/openwisp-utils

Length of output: 83


🏁 Script executed:

# Search for Django in install_requires or anywhere in setup.py
rg "django" setup.py -i -A 2 -B 2

Repository: openwisp/openwisp-utils

Length of output: 1075


🏁 Script executed:

# Check if there's a pyproject.toml or setup.cfg that might specify Python/Django versions
fd -e "pyproject.toml" -e "setup.cfg" -e ".github" | head -20

Repository: openwisp/openwisp-utils

Length of output: 49


🏁 Script executed:

# Check for GitHub workflows or CI configuration that shows tested Django versions
find .github -name "*.yml" -o -name "*.yaml" 2>/dev/null | head -5

Repository: openwisp/openwisp-utils

Length of output: 202


🌐 Web query:

django-model-utils 4.5 Django version compatibility support

💡 Result:

For django-model-utils 4.5.x (4.5.0 / 4.5.1):

  • Minimum Django: Django 3.2+ (the project states “django-model-utils supports Django 3.2+”). [1]
  • Explicitly confirmed supported Django series in the 4.5 line: Django 3.2, Django 4.2, and Django 5.0 (Django 4.2 and 5.0 support was added/confirmed in 4.4.0 and carries forward into 4.5.x). [2]

Notably, formal support for Django 5.1 was added later in django-model-utils 5.0.0, not in 4.5.x. [1]

Sources: [1] [2]


🏁 Script executed:

# Check the CI workflow to see what Django versions are being tested
cat .github/workflows/ci.yml

Repository: openwisp/openwisp-utils

Length of output: 2841


DALF 0.7.x is incompatible with Django 4.2 and Python 3.10 in the repo's test matrix.

DALF 0.7.x requires Django 5.0+ (supports Django 5.0, 5.1, 5.2) and Python >=3.11. However, this repository's CI explicitly tests Django 4.2 with Python 3.10 and 3.11. Installing this dependency will prevent users on Django 4.2 or Python 3.10 from using openwisp-utils. Either drop support for Django 4.2 and Python 3.10 in the CI matrix, or choose a different dependency version that supports the broader version range.

🤖 Prompt for AI Agents
In `@setup.py` at line 30, The dependency constraint "dalf>=0.7.0,<1.0.0" in
setup.py is incompatible with Django 4.2 and Python 3.10; update the requirement
or CI so tests match supported versions: either change the requirement string in
setup.py (replace "dalf>=0.7.0,<1.0.0" with a version range that supports Django
4.2/Python 3.10, e.g. a pre-0.7 line such as "dalf>=0.6.0,<0.7.0" or another
known compatible version) or adjust the repository CI matrix to drop Django 4.2
and Python 3.10; edit the exact token "dalf>=0.7.0,<1.0.0" to apply the chosen
fix.

"swapper~=1.4.0",
# allow wider range here to avoid interfering with other modules
"urllib3>=2.0.0,<3.0.0",
Expand Down
2 changes: 1 addition & 1 deletion tests/openwisp2/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"django.contrib.sites",
# admin
"django.contrib.admin",
"admin_auto_filters",
"dalf",
# rest framework
"rest_framework",
"drf_yasg",
Expand Down
Loading
Loading