Skip to content

Commit

Permalink
[fix/feature] Fix autocomplete filter
Browse files Browse the repository at this point in the history
[fix] Fixed "None" option of AutocompleteFilter
AutocompleteFilter shows "None" option only if the field is
nullable.

[feature] Added setting to make AutocompleteFilter view configurable
Added "OPENWISP_AUTOCOMPLETE_FILTER_VIEW" project setting to allow
configuring the view.

[fix] Don't show null option on reverse relation
  • Loading branch information
pandafy committed Dec 5, 2022
1 parent fcbbf9a commit bf1d7d7
Show file tree
Hide file tree
Showing 8 changed files with 91 additions and 18 deletions.
11 changes: 11 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1811,6 +1811,17 @@ Sets the soft time limit for celery tasks using
Sets the hard time limit for celery tasks using
`OpenwispCeleryTask <#openwisp_utilstasksopenwispcelerytask>`_.

``OPENWISP_AUTOCOMPLETE_FILTER_VIEW``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+---------+-------------------------------------------------------------+
| type | ``str`` |
+---------+-------------------------------------------------------------+
| default | ``'openwisp_utils.admin_theme.views.AutocompleteJsonView'`` |
+---------+-------------------------------------------------------------+

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

Installing for development
--------------------------

Expand Down
5 changes: 3 additions & 2 deletions openwisp_utils/admin_theme/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
from django.conf import settings
from django.contrib import admin
from django.urls import path
from django.utils.module_loading import import_string
from django.utils.translation import gettext_lazy

from . import settings as app_settings
from .dashboard import get_dashboard_context
from .views import AutocompleteJsonView

logger = logging.getLogger(__name__)

Expand All @@ -31,10 +31,11 @@ def index(self, request, extra_context=None):
return super().index(request, extra_context=context)

def get_urls(self):
autocomplete_view = import_string(app_settings.AUTOCOMPLETE_FILTER_VIEW)
return [
path(
'ow-auto-filter/',
self.admin_view(AutocompleteJsonView.as_view(admin_site=self)),
self.admin_view(autocomplete_view.as_view(admin_site=self)),
name='ow-auto-filter',
),
] + super().get_urls()
Expand Down
5 changes: 5 additions & 0 deletions openwisp_utils/admin_theme/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,8 @@
)

OPENWISP_HTML_EMAIL = getattr(settings, 'OPENWISP_HTML_EMAIL', True)
AUTOCOMPLETE_FILTER_VIEW = getattr(
settings,
'OPENWISP_AUTOCOMPLETE_FILTER_VIEW',
'openwisp_utils.admin_theme.views.AutocompleteJsonView',
)
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{% load i18n %}
<div class="auto-filter ow-filter">
<div class="ow-filter auto-filter">
{% include 'django-admin-autocomplete-filter/autocomplete-filter.html' %}
<div class="auto-filter-choices"></div>
</div>
8 changes: 7 additions & 1 deletion openwisp_utils/admin_theme/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ def get(self, request, *args, **kwargs):
context = self.get_context_data()
# Add option for filtering objects with None field.
results = []
if not self.term or self.term == '-':
if (
getattr(self.source_field, 'null', False)
and not getattr(self.source_field, '_get_limit_choices_to_mocked', False)
and not self.term
or self.term == '-'
):
results += [{'id': 'null', 'text': '-'}]
results += [
{'id': str(obj.pk), 'text': self.display_text(obj)}
Expand All @@ -37,6 +42,7 @@ def get(self, request, *args, **kwargs):

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 {}
Expand Down
9 changes: 8 additions & 1 deletion tests/test_project/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class UserAdmin(admin.ModelAdmin):
'is_superuser',
'is_active',
]
search_fields = ('username',)


@admin.register(Book)
Expand Down Expand Up @@ -95,14 +96,20 @@ class ReverseBookFilter(AutocompleteFilter):
parameter_name = 'book'


class AutoOwnerFilter(AutocompleteFilter):
title = _('owner')
field_name = 'owner'
parameter_name = 'owner_id'


@admin.register(Shelf)
class ShelfAdmin(TimeReadonlyAdminMixin, admin.ModelAdmin):
# DO NOT CHANGE: used for testing filters
list_filter = [
ShelfFilter,
['books_type', InputFilter],
['id', InputFilter],
'owner__username',
AutoOwnerFilter,
'books_type',
ReverseBookFilter,
]
Expand Down
1 change: 1 addition & 0 deletions tests/test_project/tests/test_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,7 @@ def test_input_filter(self):

def test_ow_auto_filter_view(self):
url = reverse('admin:ow-auto-filter')
url = f'{url}?app_label=test_project&model_name=shelf&field_name=book'
user = User.objects.create(
username='operator',
password='pass',
Expand Down
68 changes: 55 additions & 13 deletions tests/test_project/tests/test_selenium.py
Original file line number Diff line number Diff line change
Expand Up @@ -500,13 +500,22 @@ def test_shelf_filter(self):
with self.subTest('Test multiple filters'):
# Select Fantasy book type
books_type_title = self._get_filter_title('type-of-book')
username_title = self._get_filter_title('username')
owner_filter_xpath = '//*[@id="select2-id-owner_id-dal-filter-container"]'
owner_filter_option_xpath = (
'//*[@id="select2-id-owner_id-dal-filter-results"]/li[4]'
)
owner_filter = self.web_driver.find_element_by_xpath(owner_filter_xpath)
books_type_title.click()
fantasy_option = self._get_filter_anchor('books_type__exact=FANTASY')
fantasy_option.click()
username_title.click()
username_option = self._get_filter_anchor('owner__username=tester2')
username_option.click()
owner_filter.click()
WebDriverWait(self.web_driver, 2).until(
EC.visibility_of_element_located((By.XPATH, owner_filter_option_xpath))
)
owner_option = self.web_driver.find_element_by_xpath(
owner_filter_option_xpath
)
owner_option.click()
filter_button = self._get_filter_button()
filter_button.click()
WebDriverWait(self.web_driver, 2).until(
Expand Down Expand Up @@ -690,7 +699,7 @@ def setUp(self):
self.admin = self._create_admin()
self.login()

def test_auto_complete_filter(self):
def test_autocomplete_shelf_filter(self):
url = reverse('admin:test_project_book_changelist')
user = self._create_user()
horror_shelf = self._create_shelf(
Expand All @@ -703,8 +712,8 @@ def test_auto_complete_filter(self):
book2 = self._create_book(name='Book 2', shelf=factual_shelf)
select_id = 'id-shelf__id-dal-filter'
filter_css_selector = f'#select2-{select_id}-container'
filter_null_option_xpath = f'//*[@id="select2-{select_id}-results"]/li[1]'
filter_option_xpath = f'//*[@id="select2-{select_id}-results"]/li[3]'
filter_options = f'//*[@id="select2-{select_id}-results"]/li'
filter_option_xpath = f'//*[@id="select2-{select_id}-results"]/li[2]'

result_xpath = '//*[@id="result_list"]/tbody/tr/th/a[contains(text(), "{}")]'
self.open(url)
Expand All @@ -726,14 +735,47 @@ def test_auto_complete_filter(self):
self.web_driver.find_element_by_xpath(result_xpath.format(book1.name))
# Book 2 is present
self.web_driver.find_element_by_xpath(result_xpath.format(book2.name))
# Test null filter
# "shelf" field is not nullable, therefore none option should be absent
self.web_driver.find_element_by_css_selector(filter_css_selector).click()
self.web_driver.find_element_by_css_selector('.select2-container--open')
for option in self.web_driver.find_elements_by_xpath(filter_options):
self.assertNotEqual(option.text, '-')

def test_autocomplete_owner_filter(self):
"""
Tests the null option of the AutocompleteFilter
"""
url = reverse('admin:test_project_shelf_changelist')
user = self._create_user()
horror_shelf = self._create_shelf(
name='Horror', books_type='HORROR', owner=self.admin
)
factual_shelf = self._create_shelf(
name='Factual', books_type='FACTUAL', owner=user
)
select_id = 'id-owner_id-dal-filter'
filter_css_selector = f'#select2-{select_id}-container'
filter_null_option_xpath = f'//*[@id="select2-{select_id}-results"]/li[1]'
result_xpath = '//*[@id="result_list"]/tbody/tr/th/a[contains(text(), "{}")]'
self.open(url)
self.assertIn(
f'<select name="owner_id" id="{select_id}" class="admin-autocomplete',
self.web_driver.page_source,
)
self.web_driver.find_element_by_css_selector(filter_css_selector).click()
self.web_driver.find_element_by_css_selector('.select2-container--open')
self.assertIn(self.admin.username, self.web_driver.page_source)
self.assertIn(user.username, self.web_driver.page_source)
self.web_driver.find_element_by_xpath(filter_null_option_xpath).click()
self.assertIn('shelf__id__isnull=true', self.web_driver.current_url)
self._get_filter_button().click()
self.assertIn('owner_id__isnull=true', self.web_driver.current_url)
with self.assertRaises(NoSuchElementException):
# Book 1 is absent
self.web_driver.find_element_by_xpath(result_xpath.format(book1.name))
# horror_shelf is absent
self.web_driver.find_element_by_xpath(
result_xpath.format(horror_shelf.name)
)
with self.assertRaises(NoSuchElementException):
# Book 2 is absent
self.web_driver.find_element_by_xpath(result_xpath.format(book2.name))
# factual_shelf absent
self.web_driver.find_element_by_xpath(
result_xpath.format(factual_shelf.name)
)

0 comments on commit bf1d7d7

Please sign in to comment.