Skip to content

Commit ddb6570

Browse files
committed
fix: add unaccent to search
adds a generic filter method that takes either all text based fields or those defined in _default_search_fields and adds them to a search combined by OR using unaccent. Also adds `unaccent` to the default text filters. resolves #109
1 parent 177dbca commit ddb6570

File tree

2 files changed

+76
-2
lines changed

2 files changed

+76
-2
lines changed

apis_ontology/filtersets.py

+69-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,52 @@
22
from apis_core.generic.filtersets import django_filters
33
from apis_core.relations.filtersets import RelationFilterSet
44
from django.db import models
5+
from django.db.models import Q, CharField, TextField
56
from apis_ontology.forms import RelationFilterSetForm, EntityFilterSetForm
7+
from django.contrib.postgres.search import SearchVector, SearchQuery
8+
9+
10+
def generic_search_filter(queryset, name, value, fields=None):
11+
"""
12+
A generic filter that searches across specified fields using unaccent__icontains with OR logic.
13+
14+
Priority for fields selection:
15+
1. Explicitly provided fields parameter
16+
2. _default_search_fields attribute on the model
17+
3. All CharField and TextField fields from the model
18+
19+
Args:
20+
queryset: The queryset to filter
21+
name: The name of the filter (not used)
22+
value: The search value
23+
fields: Optional list of specific field names to search in
24+
25+
Returns:
26+
Filtered queryset
27+
"""
28+
if not value:
29+
return queryset
30+
31+
# If no fields specified, check for _default_search_fields or use all text fields
32+
if fields is None:
33+
model = queryset.model
34+
35+
# Check if model has _default_search_fields attribute
36+
if hasattr(model, "_default_search_fields"):
37+
fields = model._default_search_fields
38+
else:
39+
# Fall back to all CharField and TextField fields
40+
fields = []
41+
for field in model._meta.get_fields():
42+
if isinstance(field, (CharField, TextField)) and not field.primary_key:
43+
fields.append(field.name)
44+
45+
# Build Q objects for each field with OR logic
46+
q_objects = Q()
47+
for field in fields:
48+
q_objects |= Q(**{f"{field}__unaccent__icontains": value})
49+
50+
return queryset.filter(q_objects)
651

752

853
class NomanslandMixinFilterSet(AbstractEntityFilterSet):
@@ -28,17 +73,30 @@ class Meta(AbstractEntityFilterSet.Meta):
2873
models.CharField: {
2974
"filter_class": django_filters.CharFilter,
3075
"extra": lambda f: {
31-
"lookup_expr": "icontains",
76+
"lookup_expr": "unaccent__icontains",
3277
},
3378
},
3479
models.TextField: {
3580
"filter_class": django_filters.CharFilter,
3681
"extra": lambda f: {
37-
"lookup_expr": "icontains",
82+
"lookup_expr": "unaccent__icontains",
3883
},
3984
},
4085
}
4186

87+
def __init__(self, *args, **kwargs):
88+
super().__init__(*args, **kwargs)
89+
for filter in self.filters.values():
90+
if (
91+
hasattr(filter, "label")
92+
and filter.label
93+
and "unaccent contains" in filter.label
94+
):
95+
filter.label = filter.label.replace("unaccent contains", "")
96+
self.filters["search"] = django_filters.CharFilter(
97+
method=generic_search_filter, label="Search"
98+
)
99+
42100

43101
class NomanslandRelationMixinFilterSet(RelationFilterSet):
44102
class Meta(RelationFilterSet.Meta):
@@ -67,3 +125,12 @@ class Meta(RelationFilterSet.Meta):
67125
},
68126
},
69127
}
128+
129+
def __init__(self, *args, **kwargs):
130+
super().__init__(*args, **kwargs)
131+
for filter in self.filters.values():
132+
if hasattr(filter, "label") and filter.label and "contains" in filter.label:
133+
filter.label = filter.label.replace("contains", "")
134+
self.filters["search"] = django_filters.CharFilter(
135+
method=generic_search_filter, label="Search"
136+
)

apis_ontology/models.py

+7
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ class Meta:
9898

9999

100100
class Person(E21_Person, VersionMixin, NomanslandMixin, AbstractEntity):
101+
_default_search_fields = ["forename", "surname"]
101102
GENDERS = [
102103
("male", "Male"),
103104
("female", "Female"),
@@ -145,6 +146,7 @@ class Meta:
145146
class Place(
146147
E53_Place, VersionMixin, NomanslandDateMixin, NomanslandMixin, AbstractEntity
147148
):
149+
_default_search_fields = ["label"]
148150
class_uri = "http://id.loc.gov/ontologies/bibframe/Place"
149151
kind = models.ForeignKey(
150152
PlaceType, blank=True, null=True, on_delete=models.SET_NULL
@@ -176,6 +178,7 @@ class Meta:
176178

177179

178180
class Institution(VersionMixin, NomanslandDateMixin, NomanslandMixin, AbstractEntity):
181+
_default_search_fields = ["name"]
179182
name = models.CharField(max_length=255)
180183
kind = models.ForeignKey(
181184
InstitutionType, blank=True, null=True, on_delete=models.SET_NULL
@@ -201,6 +204,7 @@ class Meta:
201204

202205

203206
class Event(VersionMixin, NomanslandDateMixin, NomanslandMixin, AbstractEntity):
207+
_default_search_fields = ["name"]
204208
name = models.CharField(max_length=255)
205209
kind = models.ForeignKey(
206210
EventType, blank=True, null=True, on_delete=models.SET_NULL
@@ -237,6 +241,7 @@ class Meta:
237241

238242

239243
class Work(VersionMixin, NomanslandDateMixin, NomanslandMixin, AbstractEntity):
244+
_default_search_fields = ["name"]
240245
name = models.CharField(max_length=255)
241246
kind = models.ForeignKey(WorkType, blank=True, null=True, on_delete=models.SET_NULL)
242247
subject_heading = models.ManyToManyField(SubjectHeading, blank=True)
@@ -296,6 +301,7 @@ def get_queryset(self):
296301

297302

298303
class Expression(VersionMixin, NomanslandDateMixin, NomanslandMixin, AbstractEntity):
304+
_default_search_fields = ["title"]
299305
title = models.CharField(max_length=255, blank=True, null=True)
300306
locus = models.CharField(max_length=255, blank=True, null=True)
301307
script_type_title = models.ForeignKey(
@@ -337,6 +343,7 @@ class Meta:
337343

338344

339345
class Manuscript(VersionMixin, NomanslandDateMixin, NomanslandMixin, AbstractEntity):
346+
_default_search_fields = ["name", "identifier", "description", "notes", "additions"]
340347
name = models.CharField(max_length=255, blank=True, null=True)
341348
identifier = models.CharField(max_length=255, blank=True, null=True)
342349
extent = models.CharField(max_length=255, blank=True, null=True)

0 commit comments

Comments
 (0)