Skip to content

Commit 09cedfd

Browse files
authored
fix invalid lookup params (#86)
1 parent 746ea63 commit 09cedfd

File tree

2 files changed

+92
-2
lines changed

2 files changed

+92
-2
lines changed

django_typesense/changelist.py

+71-2
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@
99
IncorrectLookupParameters,
1010
)
1111
from django.contrib.admin.views.main import ChangeList
12+
from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation
1213
from django.core.paginator import InvalidPage
13-
from django.db.models import OrderBy
14+
from django.db.models import OrderBy, OuterRef, Exists
1415
from django.utils.translation import gettext
1516
from django.utils.dateparse import parse_datetime
1617

@@ -408,4 +409,72 @@ def get_typesense_results(self, request):
408409
def get_queryset(self, request):
409410
# this is needed for admin actions that call cl.get_queryset
410411
# exporting is the currently possible way of getting records from typesense without pagination
411-
return super().get_queryset(request)
412+
# Typesense team will work on a flag to disable pagination, until then, we need a way to get this to work.
413+
# Problem happens when django finds fields only present on typesense in its filter i.e IncorrectLookupParameters
414+
# First, we collect all the declared list filters.
415+
(
416+
self.filter_specs,
417+
self.has_filters,
418+
remaining_lookup_params,
419+
filters_may_have_duplicates,
420+
self.has_active_filters,
421+
) = self.get_filters(request)
422+
# Then, we let every list filter modify the queryset to its liking.
423+
qs = self.root_queryset
424+
425+
for filter_spec in self.filter_specs:
426+
new_qs = filter_spec.queryset(request, qs)
427+
if new_qs is not None:
428+
qs = new_qs
429+
430+
lookup_params_keys = remaining_lookup_params.keys()
431+
model_field_names = set((local_field.name for local_field in self.model._meta.local_fields))
432+
for key in lookup_params_keys:
433+
if key not in model_field_names:
434+
# means k only available in typesense
435+
value = remaining_lookup_params.pop(key)
436+
new_lookup_params = self.model.collection_class.get_django_lookup(key, value)
437+
remaining_lookup_params.update(new_lookup_params)
438+
439+
try:
440+
# Finally, we apply the remaining lookup parameters from the query
441+
# string (i.e. those that haven't already been processed by the
442+
# filters).
443+
qs = qs.filter(**remaining_lookup_params)
444+
except (SuspiciousOperation, ImproperlyConfigured):
445+
# Allow certain types of errors to be re-raised as-is so that the
446+
# caller can treat them in a special way.
447+
raise
448+
except Exception as e:
449+
# Every other error is caught with a naked except, because we don't
450+
# have any other way of validating lookup parameters. They might be
451+
# invalid if the keyword arguments are incorrect, or if the values
452+
# are not in the correct type, so we might get FieldError,
453+
# ValueError, ValidationError, or ?.
454+
raise IncorrectLookupParameters(e)
455+
456+
# Apply search results
457+
qs, search_may_have_duplicates = self.model_admin.get_search_results(
458+
request,
459+
qs,
460+
self.query,
461+
)
462+
463+
# Set query string for clearing all filters.
464+
self.clear_all_filters_qs = self.get_query_string(
465+
new_params=remaining_lookup_params,
466+
remove=self.get_filters_params(),
467+
)
468+
# Remove duplicates from results, if necessary
469+
if filters_may_have_duplicates | search_may_have_duplicates:
470+
qs = qs.filter(pk=OuterRef("pk"))
471+
qs = self.root_queryset.filter(Exists(qs))
472+
473+
# Set ordering.
474+
ordering = self.get_ordering(request, qs)
475+
qs = qs.order_by(*ordering)
476+
477+
if not qs.query.select_related:
478+
qs = self.apply_select_related(qs)
479+
480+
return qs

django_typesense/collections.py

+21
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import logging
4+
from operator import methodcaller
45
from typing import Dict, Iterable, List, Union
56

67
from django.db.models import QuerySet
@@ -196,6 +197,26 @@ def get_field(cls, name) -> TypesenseField:
196197
fields = cls.get_fields()
197198
return fields[name]
198199

200+
@classmethod
201+
def get_django_lookup(cls, field, value) -> dict:
202+
"""
203+
Get the lookup that would have been used for this field in django. Expects to find a method on
204+
the collection called `get_FIELD_lookup` otherwise a NotImplementedError is raised
205+
206+
Args:
207+
collection_field_name: the name of the field in the collection
208+
value: the value to look for
209+
210+
Returns:
211+
A dictionary of the fields to the value.
212+
"""
213+
214+
if "get_%s_lookup" % field not in cls.__dict__:
215+
raise NotImplementedError
216+
217+
method = methodcaller("get_%s_lookup" % field, value)
218+
return method(cls)
219+
199220
@cached_property
200221
def schema_fields(self) -> list:
201222
"""

0 commit comments

Comments
 (0)