Skip to content

Commit bd6da44

Browse files
authored
triv: #26231 added search bar also to v1 filters, added support for fields outside of list fields (#138)
* triv: #no-task fixing admin * triv: #26231 added search bar also to v1 filters, added support for fields outside of list fields * triv: #26231 added search bar also to v1 filters, added support for fields outside of list fields * triv: #26231 moving search bar to filters * triv: #26231 added unaccent and moved get_search_lookup method for better overriding possibilities * triv: #26231 added check if unaccent is available
1 parent 9e97f7c commit bd6da44

5 files changed

Lines changed: 132 additions & 64 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "django-smartbase-admin"
3-
version = "1.3.1"
3+
version = "1.3.2"
44
description = ""
55
authors = ["SmartBase <info@smartbase.sk>"]
66
readme = "README.md"

src/django_smartbase_admin/actions/admin_action_list.py

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -347,21 +347,31 @@ def get_search_results(self, request, queryset, search_term):
347347

348348
# Apply keyword searches.
349349
def construct_search(field_name):
350-
if field_name.startswith("^"):
351-
return "%s__istartswith" % field_name[1:]
352-
elif field_name.startswith("="):
353-
return "%s__iexact" % field_name[1:]
354-
elif field_name.startswith("@"):
355-
return "%s__search" % field_name[1:]
356-
# Otherwise, use the field with icontains.
357-
return "%s__icontains" % field_name
350+
prefix = field_name[0] if field_name and field_name[0] in "^=@" else ""
351+
raw_field_name = field_name[1:] if prefix else field_name
352+
return self.view.get_search_lookup(request, raw_field_name, prefix)
358353

359354
search_fields = self.get_search_fields(request)
360-
if search_fields and search_term:
361-
orm_lookups = [
362-
construct_search(str(search_field.filter_field))
363-
for search_field in search_fields
364-
]
355+
search_fields_definition = list(self.view.get_search_fields(request) or [])
356+
if search_fields_definition and search_term:
357+
search_field_map = {
358+
field.name: str(field.filter_field) for field in search_fields
359+
}
360+
orm_lookups = []
361+
for configured_search_field in search_fields_definition:
362+
configured_search_field = str(configured_search_field)
363+
if not configured_search_field:
364+
continue
365+
prefix = (
366+
configured_search_field[0]
367+
if configured_search_field[0] in "^=@"
368+
else ""
369+
)
370+
raw_field_name = (
371+
configured_search_field[1:] if prefix else configured_search_field
372+
)
373+
lookup_field = search_field_map.get(raw_field_name, raw_field_name)
374+
orm_lookups.append(construct_search(f"{prefix}{lookup_field}"))
365375
term_queries = []
366376
for bit in smart_split(search_term):
367377
if bit.startswith(('"', "'")) and bit[0] == bit[-1]:

src/django_smartbase_admin/engine/admin_base_view.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,32 @@ class SBAdminBaseListView(SBAdminBaseView):
456456
filters_version = None
457457
sbadmin_actions_initialized = False
458458
sbadmin_list_action_class = SBAdminListAction
459+
pg_unaccent_ext_cache = {}
460+
461+
@classmethod
462+
def _postgres_unaccent_extension_available(cls) -> bool:
463+
from django.conf import settings
464+
from django.db import connection
465+
466+
if connection.vendor != "postgresql":
467+
return False
468+
if "django.contrib.postgres" not in settings.INSTALLED_APPS:
469+
return False
470+
alias = connection.alias
471+
cached = cls.pg_unaccent_ext_cache.get(alias)
472+
if cached is not None:
473+
return cached
474+
try:
475+
with connection.cursor() as cursor:
476+
cursor.execute(
477+
"SELECT 1 FROM pg_extension WHERE extname = %s LIMIT 1",
478+
["unaccent"],
479+
)
480+
available = bool(cursor.fetchone())
481+
except Exception:
482+
available = False
483+
cls.pg_unaccent_ext_cache[alias] = available
484+
return available
459485

460486
def get_sbadmin_nested(self, request) -> dict | None:
461487
"""Return the nested config dict for this view, or ``None`` for a flat list.
@@ -599,6 +625,17 @@ def get_search_fields(self, request):
599625
else:
600626
return []
601627

628+
def get_search_lookup(self, request, field_name: str, prefix: str = "") -> str:
629+
if prefix == "^":
630+
return f"{field_name}__istartswith"
631+
if prefix == "=":
632+
return f"{field_name}__iexact"
633+
if prefix == "@":
634+
return f"{field_name}__search"
635+
if self._postgres_unaccent_extension_available():
636+
return f"{field_name}__unaccent__icontains"
637+
return f"{field_name}__icontains"
638+
602639
def get_list_ordering(self, request) -> Iterable[str] | list:
603640
return self.ordering or []
604641

src/django_smartbase_admin/templates/sb_admin/actions/partials/tabulator_header_v1.html

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,8 @@
3131
</div>
3232
{% endblock %}
3333
</div>
34-
3534
<div id="filters-collapse" class="collapse border-t border-dark-200 max-sm:overflow-x-auto custom-scrollbar{% if collapse_opened %} show{% endif %}">
36-
{% include 'sb_admin/components/filters.html' with filters=content_context.filters %}
35+
{% include 'sb_admin/components/filters.html' with filters=content_context.filters show_full_text_search=content_context.search_fields %}
3736
</div>
3837
{% endwith %}
3938
{% endblock %}
Lines changed: 70 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,76 @@
11
{% load i18n %}
22

3-
<ul class="py-12 px-16 flex md:flex-wrap gap-8 max-sm:overflow-x-auto">
4-
{% for filter_field in filters %}
5-
<li class="filter-wrapper relative max-sm:flex-shrink-0{% if not all_filters_visible %} hidden{% endif %}"
6-
data-filter-input-name="{{ filter_field.filter_widget.input_name }}" {% if all_filters_visible %}data-all-filters-visible{% endif %}
7-
id="{{ filter_field.filter_widget.input_id }}-wrapper">
8-
<button
9-
data-bs-toggle="dropdown"
10-
aria-expanded="false"
11-
{% if filter_field.filter_widget.close_dropdown_on_change %}data-sbadmin-close-on-change="1"{% endif %}
12-
class="js-filter-dropdown-button empty {% if default_button %}btn{% else %}filter-dropdown-button{% endif %}">
13-
<span>
14-
{{ filter_field.title|capfirst }}: <span
15-
id="{{ filter_field.filter_widget.input_id }}-value">{{ filter_field.filter_widget.get_default_label|default_if_none:"" }}</span>
16-
</span>
17-
<svg class="w-16 h-16" title="{% trans 'Remove' %}" onclick="window.SBAdminTable['{{ view_id }}'].moduleInstances.filterModule.clearFilter('{{ filter_field.filter_field }}')">
18-
<use xlink:href="#Close-small"></use>
19-
</svg>
20-
<svg class="w-16 h-16 ml-4">
21-
<use xlink:href="#Down"></use>
3+
<div class="py-12 px-16 flex items-start gap-8 max-sm:overflow-x-auto">
4+
<ul class="flex flex-wrap gap-8 flex-1 min-w-0">
5+
{% for filter_field in filters %}
6+
<li class="filter-wrapper relative max-sm:flex-shrink-0{% if not all_filters_visible %} hidden{% endif %}"
7+
data-filter-input-name="{{ filter_field.filter_widget.input_name }}" {% if all_filters_visible %}data-all-filters-visible{% endif %}
8+
id="{{ filter_field.filter_widget.input_id }}-wrapper">
9+
<button
10+
data-bs-toggle="dropdown"
11+
aria-expanded="false"
12+
{% if filter_field.filter_widget.close_dropdown_on_change %}data-sbadmin-close-on-change="1"{% endif %}
13+
class="js-filter-dropdown-button empty {% if default_button %}btn{% else %}filter-dropdown-button{% endif %}">
14+
<span>
15+
{{ filter_field.title|capfirst }}: <span
16+
id="{{ filter_field.filter_widget.input_id }}-value">{{ filter_field.filter_widget.get_default_label|default_if_none:"" }}</span>
17+
</span>
18+
<svg class="w-16 h-16" title="{% trans 'Remove' %}" onclick="window.SBAdminTable['{{ view_id }}'].moduleInstances.filterModule.clearFilter('{{ filter_field.filter_field }}')">
19+
<use xlink:href="#Close-small"></use>
20+
</svg>
21+
<svg class="w-16 h-16 ml-4">
22+
<use xlink:href="#Down"></use>
23+
</svg>
24+
</button>
25+
{% include filter_field.filter_widget.template_name with filter_widget=filter_field.filter_widget %}
26+
</li>
27+
{% endfor %}
28+
{% if not all_filters_visible %}
29+
<li class="relative max-sm:-order-1">
30+
<button
31+
data-bs-toggle="dropdown"
32+
aria-expanded="false"
33+
class="btn btn-small rounded-full bg-dark-100 shadow-none">
34+
<span>
35+
{% trans 'Add' %}
36+
</span>
37+
<svg class="w-16 h-16 ml-4">
38+
<use xlink:href="#Plus"></use>
39+
</svg>
40+
</button>
41+
<div class="dropdown-menu">
42+
<ul>
43+
{% for filter_field in filters %}
44+
<li>
45+
<button type="button"
46+
onclick="window.SBAdminTable['{{ view_id }}'].moduleInstances.filterModule.showFilter('{{ filter_field.filter_field }}')"
47+
class="w-full dropdown-menu-link">{{ filter_field.title }}</button>
48+
</li>
49+
{% endfor %}
50+
</ul>
51+
</div>
52+
</li>
53+
{% endif %}
54+
</ul>
55+
{% if show_full_text_search %}
56+
<div class="relative flex-shrink-0 ml-auto max-sm:ml-0">
57+
<input type="text"
58+
name="{{ content_context.const.TABLE_PARAMS_FULL_TEXT_SEARCH }}"
59+
class="input pl-38 pr-38 w-320 max-sm:w-280"
60+
form="{{ view_id }}-filter-form"
61+
id="{{ view_id }}-{{ content_context.const.TABLE_PARAMS_FULL_TEXT_SEARCH }}"
62+
placeholder="{{ content_context.search_field_placeholder }}">
63+
<div class="absolute pl-10 left-0 top-0 bottom-0 flex items-center gap-8 rounded-r pointer-events-none">
64+
<svg class="w-20 h-20">
65+
<use xlink:href="#Search"></use>
2266
</svg>
23-
</button>
24-
{% include filter_field.filter_widget.template_name with filter_widget=filter_field.filter_widget %}
25-
</li>
26-
{% endfor %}
27-
{% if not all_filters_visible %}
28-
<li class="relative max-sm:-order-1">
29-
<button
30-
data-bs-toggle="dropdown"
31-
aria-expanded="false"
32-
class="btn btn-small rounded-full bg-dark-100 shadow-none">
33-
<span>
34-
{% trans 'Add' %}
35-
</span>
36-
<svg class="w-16 h-16 ml-4">
37-
<use xlink:href="#Plus"></use>
67+
</div>
68+
<div class="cursor-pointer absolute pr-10 right-0 top-0 bottom-0 flex items-center gap-8 rounded-r">
69+
<svg onclick="window.SBAdminTable['{{ view_id }}'].moduleInstances.filterModule.clearFilter('{{ content_context.const.TABLE_PARAMS_FULL_TEXT_SEARCH }}')"
70+
class="w-20 h-20">
71+
<use xlink:href="#Close-small"></use>
3872
</svg>
39-
</button>
40-
<div class="dropdown-menu">
41-
<ul>
42-
{% for filter_field in filters %}
43-
<li>
44-
<button type="button"
45-
onclick="window.SBAdminTable['{{ view_id }}'].moduleInstances.filterModule.showFilter('{{ filter_field.filter_field }}')"
46-
class="w-full dropdown-menu-link">{{ filter_field.title }}</button>
47-
</li>
48-
{% endfor %}
49-
</ul>
5073
</div>
51-
</li>
74+
</div>
5275
{% endif %}
53-
<div class="lg:hidden order-1 w-8 flex-shrink-0"></div>
54-
</ul>
76+
</div>

0 commit comments

Comments
 (0)