-
-
Notifications
You must be signed in to change notification settings - Fork 96
[change] Migrate from django-admin-autocomplete-filter to dalf #582 #585
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
a6c0e64
dbb363f
1a8425b
85091a5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,3 @@ | ||
| *.min.js | ||
| *.min.css | ||
|
|
| 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 _ | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
@@ -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) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
@@ -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
+134
|
||||||||||||||||||||||||||||
| 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 | |
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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.pyRepository: 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 -200Repository: 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 5Repository: 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 3Repository: 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 5Repository: 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 2Repository: 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 testRepository: openwisp/openwisp-utils
Length of output: 140
🏁 Script executed:
#!/bin/bash
# Search for test files related to filters
fd -n "test.*filter" --type fRepository: 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 -30Repository: 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 5Repository: 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 -150Repository: 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 10Repository: 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 2Repository: 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)
EOFRepository: 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 5Repository: 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 -20Repository: 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.pyRepository: 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 -5Repository: 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 dalfRepository: 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 3Repository: 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.pyRepository: 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}")
EOFRepository: 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 2Repository: 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 -80Repository: 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 -150Repository: 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 2Repository: 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.pyRepository: 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)
EOFRepository: 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 2Repository: 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 5Repository: 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 -50Repository: 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 2Repository: 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=pyRepository: 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)
EOFRepository: 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.pyRepository: 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 20Repository: 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 2Repository: 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 -100Repository: 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)
EOFRepository: 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 -30Repository: 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.pyRepository: 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.pyRepository: 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.
| 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> |
| 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() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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", | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: For dalf (django-admin-list-filter) 0.7.x:
Sources: PyPI project page / metadata [1] and the upstream GitHub README stating Django 5+ support [2]. [1] (pypi.org) Citations:
🏁 Script executed: # Check the setup.py for Django version requirements in this repository
cat setup.py | head -50Repository: 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 -20Repository: openwisp/openwisp-utils Length of output: 49 🏁 Script executed: cat setup.py | tail -100Repository: 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 2Repository: 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 -20Repository: 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 -5Repository: openwisp/openwisp-utils Length of output: 202 🌐 Web query:
💡 Result: For django-model-utils 4.5.x (4.5.0 / 4.5.1):
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.ymlRepository: 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 |
||
| "swapper~=1.4.0", | ||
| # allow wider range here to avoid interfering with other modules | ||
| "urllib3>=2.0.0,<3.0.0", | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid catching bare
Exceptionand silently passing; also, redundant import.Two issues:
FieldListFilteris already imported at line 3, so the local import is unnecessary.Exceptiontypes 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
🧰 Tools
🪛 Ruff (0.14.14)
[error] 131-132:
try-except-passdetected, consider logging the exception(S110)
[warning] 131-131: Do not catch blind exception:
Exception(BLE001)
🤖 Prompt for AI Agents