drf-react-by-schema 0.28.2__tar.gz → 0.28.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/PKG-INFO +10 -1
- {drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/README.rst +9 -0
- drf_react_by_schema-0.28.3/drf_react_by_schema/filters.py +212 -0
- {drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema.egg-info/PKG-INFO +10 -1
- {drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/setup.cfg +1 -1
- drf_react_by_schema-0.28.2/drf_react_by_schema/filters.py +0 -112
- {drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/LICENSE +0 -0
- {drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/MANIFEST.in +0 -0
- {drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/__init__.py +0 -0
- {drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/admin.py +0 -0
- {drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/apps.py +0 -0
- {drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/auth.py +0 -0
- {drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/autocomplete_serializers.py +0 -0
- {drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/autocomplete_viewsets.py +0 -0
- {drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/checks.py +0 -0
- {drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/date_utils.py +0 -0
- {drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/fields/__init__.py +0 -0
- {drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/fields/array.py +0 -0
- {drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/fields/boolean.py +0 -0
- {drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/fields/date.py +0 -0
- {drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/fields/file.py +0 -0
- {drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/fields/geometry.py +0 -0
- {drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/fields/number.py +0 -0
- {drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/fields/relational.py +0 -0
- {drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/fields/serializers.py +0 -0
- {drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/fields/text.py +0 -0
- {drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/management/__init__.py +0 -0
- {drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/management/commands/__init__.py +0 -0
- {drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/management/commands/sync_react.py +0 -0
- {drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/metadata.py +0 -0
- {drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/migrations/__init__.py +0 -0
- {drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/mixins.py +0 -0
- {drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/models.py +0 -0
- {drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/pagination.py +0 -0
- {drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/patches/__init__.py +0 -0
- {drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/patches/drf_serializers.py +0 -0
- {drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/renderers.py +0 -0
- {drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/schema_bundle.py +0 -0
- {drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/serializers.py +0 -0
- {drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/tests.py +0 -0
- {drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/typescript_generator.py +0 -0
- {drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/urls.py +0 -0
- {drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/utils.py +0 -0
- {drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/utils_cache.py +0 -0
- {drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/views.py +0 -0
- {drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/viewsets.py +0 -0
- {drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema.egg-info/SOURCES.txt +0 -0
- {drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema.egg-info/dependency_links.txt +0 -0
- {drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema.egg-info/requires.txt +0 -0
- {drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema.egg-info/top_level.txt +0 -0
- {drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: drf_react_by_schema
|
|
3
|
-
Version: 0.28.
|
|
3
|
+
Version: 0.28.3
|
|
4
4
|
Summary: A Django app to easily integrate with React from models.
|
|
5
5
|
Home-page: https://gitlab.com/eita/drf-react-by-schema/drf-react-by-schema-django
|
|
6
6
|
Author: Cooperativa EITA
|
|
@@ -136,6 +136,15 @@ with the package's::
|
|
|
136
136
|
``unaccent`` is applied only to text fields; dates, numbers and booleans keep
|
|
137
137
|
DRF's default lookup, because ``unaccent`` raises on them.
|
|
138
138
|
|
|
139
|
+
The filter also orders the hits by relevance. It concatenates the short text
|
|
140
|
+
(``CharField``) paths of ``search_fields``, in order, and scores the whole query
|
|
141
|
+
against that label: exact match (100), prefix (80), substring (60). The first
|
|
142
|
+
path is scored on its own as well, so ``nome`` keeps its exact/prefix bonus even
|
|
143
|
+
when the label also contains phone/e-mail fields. Records that only match all
|
|
144
|
+
the terms across fields keep score 0 and are ordered by ``pk`` as a stable
|
|
145
|
+
tiebreaker. An explicit ``?ordering=`` always wins over relevance. ``TextField``
|
|
146
|
+
paths (long free text such as notes) stay out of the label.
|
|
147
|
+
|
|
139
148
|
The filter is **opt-in**: apps that keep DRF's ``SearchFilter`` are unaffected
|
|
140
149
|
and do not need the extension. It probes the backend, ``INSTALLED_APPS`` and the
|
|
141
150
|
database extension once per connection alias. If the extension is missing — or
|
|
@@ -104,6 +104,15 @@ with the package's::
|
|
|
104
104
|
``unaccent`` is applied only to text fields; dates, numbers and booleans keep
|
|
105
105
|
DRF's default lookup, because ``unaccent`` raises on them.
|
|
106
106
|
|
|
107
|
+
The filter also orders the hits by relevance. It concatenates the short text
|
|
108
|
+
(``CharField``) paths of ``search_fields``, in order, and scores the whole query
|
|
109
|
+
against that label: exact match (100), prefix (80), substring (60). The first
|
|
110
|
+
path is scored on its own as well, so ``nome`` keeps its exact/prefix bonus even
|
|
111
|
+
when the label also contains phone/e-mail fields. Records that only match all
|
|
112
|
+
the terms across fields keep score 0 and are ordered by ``pk`` as a stable
|
|
113
|
+
tiebreaker. An explicit ``?ordering=`` always wins over relevance. ``TextField``
|
|
114
|
+
paths (long free text such as notes) stay out of the label.
|
|
115
|
+
|
|
107
116
|
The filter is **opt-in**: apps that keep DRF's ``SearchFilter`` are unaffected
|
|
108
117
|
and do not need the extension. It probes the backend, ``INSTALLED_APPS`` and the
|
|
109
118
|
database extension once per connection alias. If the extension is missing — or
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
from django.apps import apps
|
|
2
|
+
from django.db import connections
|
|
3
|
+
from django.db.models import Case, CharField, F, IntegerField, TextField, Value, When
|
|
4
|
+
from django.db.models.functions import Concat, Greatest, Trim
|
|
5
|
+
from rest_framework.filters import OrderingFilter, SearchFilter
|
|
6
|
+
from rest_framework.settings import api_settings
|
|
7
|
+
|
|
8
|
+
from . import utils
|
|
9
|
+
|
|
10
|
+
# Only these field types support the unaccent lookup.
|
|
11
|
+
TEXT_FIELD_CLASSES = (CharField, TextField)
|
|
12
|
+
|
|
13
|
+
# DRF lookups the filter converts to their unaccent-aware counterparts.
|
|
14
|
+
UNACCENT_LOOKUPS = ("icontains", "istartswith", "iexact")
|
|
15
|
+
|
|
16
|
+
# Per-database result of the unaccent probe (one query per alias per process).
|
|
17
|
+
_UNACCENT_AVAILABLE = {}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def reset_unaccent_cache():
|
|
21
|
+
"""Forget the unaccent probe, e.g. after installing the extension."""
|
|
22
|
+
_UNACCENT_AVAILABLE.clear()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def unaccent_available(connection):
|
|
26
|
+
"""Whether ``connection`` can run the ``unaccent`` lookup.
|
|
27
|
+
|
|
28
|
+
False on non-PostgreSQL backends, without ``django.contrib.postgres`` (which
|
|
29
|
+
registers the lookup), or when the extension is missing from the database.
|
|
30
|
+
"""
|
|
31
|
+
if connection.vendor != "postgresql":
|
|
32
|
+
return False
|
|
33
|
+
if not apps.is_installed("django.contrib.postgres"):
|
|
34
|
+
return False
|
|
35
|
+
alias = connection.alias
|
|
36
|
+
if alias not in _UNACCENT_AVAILABLE:
|
|
37
|
+
try:
|
|
38
|
+
with connection.cursor() as cursor:
|
|
39
|
+
cursor.execute(
|
|
40
|
+
"SELECT 1 FROM pg_extension WHERE extname = 'unaccent'"
|
|
41
|
+
)
|
|
42
|
+
_UNACCENT_AVAILABLE[alias] = cursor.fetchone() is not None
|
|
43
|
+
except Exception:
|
|
44
|
+
_UNACCENT_AVAILABLE[alias] = False
|
|
45
|
+
return _UNACCENT_AVAILABLE[alias]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class DRFReactBySchemaSearchFilter(SearchFilter):
|
|
49
|
+
"""``SearchFilter`` with accent-insensitive matching and relevance order.
|
|
50
|
+
|
|
51
|
+
Non-text fields keep DRF's default lookup: ``unaccent`` only accepts text,
|
|
52
|
+
so applying it to a date, number or boolean raises ``FieldError``.
|
|
53
|
+
|
|
54
|
+
Accent-insensitive search needs PostgreSQL with the ``unaccent`` extension
|
|
55
|
+
enabled and ``django.contrib.postgres`` in ``INSTALLED_APPS``. When either
|
|
56
|
+
is missing (or the database is not PostgreSQL), the filter silently falls
|
|
57
|
+
back to DRF's lookups, so search still works (accent-sensitive).
|
|
58
|
+
|
|
59
|
+
Hits are ranked by how the whole query matches the concatenation of the
|
|
60
|
+
short text ``search_fields`` (the label): exact, then prefix, then
|
|
61
|
+
substring. Every match that does not reach those tiers keeps score 0.
|
|
62
|
+
The client's ``?ordering=`` always wins over relevance.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
# Resolved per request by ``filter_queryset``; None on direct calls.
|
|
66
|
+
_unaccent_enabled = None
|
|
67
|
+
|
|
68
|
+
def filter_queryset(self, request, queryset, view):
|
|
69
|
+
self._unaccent_enabled = unaccent_available(connections[queryset.db])
|
|
70
|
+
queryset = super().filter_queryset(request, queryset, view)
|
|
71
|
+
return self._order_by_relevance(request, queryset, view)
|
|
72
|
+
|
|
73
|
+
def construct_search(self, field_name, queryset):
|
|
74
|
+
lookup = super().construct_search(field_name, queryset)
|
|
75
|
+
unaccent_enabled = self._unaccent_enabled
|
|
76
|
+
if unaccent_enabled is None:
|
|
77
|
+
unaccent_enabled = unaccent_available(connections[queryset.db])
|
|
78
|
+
if not unaccent_enabled:
|
|
79
|
+
return lookup
|
|
80
|
+
if not self._is_text_field(queryset.model, field_name):
|
|
81
|
+
return lookup
|
|
82
|
+
for standard_lookup in UNACCENT_LOOKUPS:
|
|
83
|
+
if lookup.endswith(f"__{standard_lookup}"):
|
|
84
|
+
return lookup[: -len(standard_lookup)] + f"unaccent__{standard_lookup}"
|
|
85
|
+
return lookup
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
def _resolve_field(model, field_path):
|
|
89
|
+
"""Resolve the terminal model field of a ``search_fields`` path."""
|
|
90
|
+
# Strip DRF prefix modifiers: ^, =, @, $
|
|
91
|
+
parts = str(field_path).lstrip("^=@$").split("__")
|
|
92
|
+
try:
|
|
93
|
+
for part in parts[:-1]:
|
|
94
|
+
model = model._meta.get_field(part).related_model
|
|
95
|
+
return model._meta.get_field(parts[-1])
|
|
96
|
+
except Exception:
|
|
97
|
+
# Unknown paths and non-relational lookups keep the standard lookup.
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def _is_text_field(cls, model, field_path):
|
|
102
|
+
"""Whether the terminal field in ``field_path`` is a text field."""
|
|
103
|
+
return isinstance(cls._resolve_field(model, field_path), TEXT_FIELD_CLASSES)
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def _is_char_field(cls, model, field_path):
|
|
107
|
+
return isinstance(cls._resolve_field(model, field_path), CharField)
|
|
108
|
+
|
|
109
|
+
@classmethod
|
|
110
|
+
def _label_paths(cls, model, search_fields):
|
|
111
|
+
"""``CharField`` paths whose concatenation is the searchable label.
|
|
112
|
+
|
|
113
|
+
``TextField`` stays out: the label is for short identifiers (name, code,
|
|
114
|
+
e-mail), not long free text such as ``dados``/``observacoes``.
|
|
115
|
+
"""
|
|
116
|
+
paths = []
|
|
117
|
+
seen = set()
|
|
118
|
+
for search_field in search_fields:
|
|
119
|
+
path = str(search_field).lstrip("^=@$")
|
|
120
|
+
if path in seen or not cls._is_char_field(model, path):
|
|
121
|
+
continue
|
|
122
|
+
seen.add(path)
|
|
123
|
+
paths.append(path)
|
|
124
|
+
return paths
|
|
125
|
+
|
|
126
|
+
@staticmethod
|
|
127
|
+
def _label_expression(paths):
|
|
128
|
+
"""Concatenate the label paths in ``search_fields`` order."""
|
|
129
|
+
if not paths:
|
|
130
|
+
return None
|
|
131
|
+
if len(paths) == 1:
|
|
132
|
+
return Trim(F(paths[0]))
|
|
133
|
+
parts = []
|
|
134
|
+
for index, path in enumerate(paths):
|
|
135
|
+
if index:
|
|
136
|
+
parts.append(Value(" "))
|
|
137
|
+
parts.append(F(path))
|
|
138
|
+
return Trim(Concat(*parts, output_field=CharField()))
|
|
139
|
+
|
|
140
|
+
def _order_by_relevance(self, request, queryset, view):
|
|
141
|
+
"""Order hits by label match, unless the client sent ``?ordering=``."""
|
|
142
|
+
if request.query_params.get(api_settings.ORDERING_PARAM):
|
|
143
|
+
return queryset
|
|
144
|
+
search_terms = self.get_search_terms(request)
|
|
145
|
+
if not search_terms:
|
|
146
|
+
return queryset
|
|
147
|
+
paths = self._label_paths(
|
|
148
|
+
queryset.model, self.get_search_fields(view, request)
|
|
149
|
+
)
|
|
150
|
+
label = self._label_expression(paths)
|
|
151
|
+
if label is None:
|
|
152
|
+
return queryset
|
|
153
|
+
|
|
154
|
+
query = " ".join(search_terms)
|
|
155
|
+
# The probe already checks the extension; re-check the app so a
|
|
156
|
+
# mocked/partial setup (lookup not registered) falls back to the
|
|
157
|
+
# standard lookups.
|
|
158
|
+
unaccent = self._unaccent_enabled and apps.is_installed(
|
|
159
|
+
"django.contrib.postgres"
|
|
160
|
+
)
|
|
161
|
+
label_lookup = "_drbs_search_label"
|
|
162
|
+
if unaccent:
|
|
163
|
+
# Bilateral transform: unaccents both the label and the query.
|
|
164
|
+
label_lookup += "__unaccent"
|
|
165
|
+
label_rank = Case(
|
|
166
|
+
When(**{f"{label_lookup}__iexact": query}, then=Value(100)),
|
|
167
|
+
When(**{f"{label_lookup}__istartswith": query}, then=Value(80)),
|
|
168
|
+
When(**{f"{label_lookup}__icontains": query}, then=Value(60)),
|
|
169
|
+
default=Value(0),
|
|
170
|
+
output_field=IntegerField(),
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# A single field may hold the whole query (e.g. ``nome``) even when the
|
|
174
|
+
# concatenated label is polluted by other text fields (phone, e-mail).
|
|
175
|
+
# Only the first label path is scored: it is the primary identifier,
|
|
176
|
+
# so a note or e-mail starting with the query cannot outrank a name.
|
|
177
|
+
primary_lookup = paths[0] + ("__unaccent" if unaccent else "")
|
|
178
|
+
primary_rank = Case(
|
|
179
|
+
When(**{f"{primary_lookup}__iexact": query}, then=Value(100)),
|
|
180
|
+
When(**{f"{primary_lookup}__istartswith": query}, then=Value(80)),
|
|
181
|
+
default=Value(0),
|
|
182
|
+
output_field=IntegerField(),
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
rank = Greatest(label_rank, primary_rank)
|
|
186
|
+
return (
|
|
187
|
+
queryset.annotate(_drbs_search_label=label)
|
|
188
|
+
.annotate(_drbs_search_rank=rank)
|
|
189
|
+
.order_by(F("_drbs_search_rank").desc(), queryset.model._meta.pk.name)
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class DRFReactBySchemaOrderingFilter(OrderingFilter):
|
|
194
|
+
def get_ordering(self, request, queryset, view):
|
|
195
|
+
"""
|
|
196
|
+
Ordering is set by a comma delimited ?ordering=... query parameter.
|
|
197
|
+
|
|
198
|
+
The `ordering` query parameter can be overridden by setting
|
|
199
|
+
the `ordering_param` value on the OrderingFilter or by
|
|
200
|
+
specifying an `ORDERING_PARAM` value in the API settings.
|
|
201
|
+
"""
|
|
202
|
+
params = request.query_params.get(self.ordering_param)
|
|
203
|
+
if params:
|
|
204
|
+
fields_raw = [param.strip() for param in params.split(",")]
|
|
205
|
+
fields = utils.serializer_to_model_fields(view, fields_raw)
|
|
206
|
+
if fields is not None:
|
|
207
|
+
ordering = self.remove_invalid_fields(queryset, fields, view, request)
|
|
208
|
+
if ordering:
|
|
209
|
+
return ordering
|
|
210
|
+
|
|
211
|
+
# No ordering was included, or all the ordering fields were invalid
|
|
212
|
+
return self.get_default_ordering(view)
|
{drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: drf-react-by-schema
|
|
3
|
-
Version: 0.28.
|
|
3
|
+
Version: 0.28.3
|
|
4
4
|
Summary: A Django app to easily integrate with React from models.
|
|
5
5
|
Home-page: https://gitlab.com/eita/drf-react-by-schema/drf-react-by-schema-django
|
|
6
6
|
Author: Cooperativa EITA
|
|
@@ -136,6 +136,15 @@ with the package's::
|
|
|
136
136
|
``unaccent`` is applied only to text fields; dates, numbers and booleans keep
|
|
137
137
|
DRF's default lookup, because ``unaccent`` raises on them.
|
|
138
138
|
|
|
139
|
+
The filter also orders the hits by relevance. It concatenates the short text
|
|
140
|
+
(``CharField``) paths of ``search_fields``, in order, and scores the whole query
|
|
141
|
+
against that label: exact match (100), prefix (80), substring (60). The first
|
|
142
|
+
path is scored on its own as well, so ``nome`` keeps its exact/prefix bonus even
|
|
143
|
+
when the label also contains phone/e-mail fields. Records that only match all
|
|
144
|
+
the terms across fields keep score 0 and are ordered by ``pk`` as a stable
|
|
145
|
+
tiebreaker. An explicit ``?ordering=`` always wins over relevance. ``TextField``
|
|
146
|
+
paths (long free text such as notes) stay out of the label.
|
|
147
|
+
|
|
139
148
|
The filter is **opt-in**: apps that keep DRF's ``SearchFilter`` are unaffected
|
|
140
149
|
and do not need the extension. It probes the backend, ``INSTALLED_APPS`` and the
|
|
141
150
|
database extension once per connection alias. If the extension is missing — or
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
from django.apps import apps
|
|
2
|
-
from django.db import connections
|
|
3
|
-
from django.db.models import CharField, TextField
|
|
4
|
-
from rest_framework.filters import OrderingFilter, SearchFilter
|
|
5
|
-
|
|
6
|
-
from . import utils
|
|
7
|
-
|
|
8
|
-
# Only these field types support the unaccent lookup.
|
|
9
|
-
TEXT_FIELD_CLASSES = (CharField, TextField)
|
|
10
|
-
|
|
11
|
-
# DRF lookups the filter converts to their unaccent-aware counterparts.
|
|
12
|
-
UNACCENT_LOOKUPS = ("icontains", "istartswith", "iexact")
|
|
13
|
-
|
|
14
|
-
# Per-database result of the unaccent probe (one query per alias per process).
|
|
15
|
-
_UNACCENT_AVAILABLE = {}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def reset_unaccent_cache():
|
|
19
|
-
"""Forget the unaccent probe, e.g. after installing the extension."""
|
|
20
|
-
_UNACCENT_AVAILABLE.clear()
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def unaccent_available(connection):
|
|
24
|
-
"""Whether ``connection`` can run the ``unaccent`` lookup.
|
|
25
|
-
|
|
26
|
-
False on non-PostgreSQL backends, without ``django.contrib.postgres`` (which
|
|
27
|
-
registers the lookup), or when the extension is missing from the database.
|
|
28
|
-
"""
|
|
29
|
-
if connection.vendor != "postgresql":
|
|
30
|
-
return False
|
|
31
|
-
if not apps.is_installed("django.contrib.postgres"):
|
|
32
|
-
return False
|
|
33
|
-
alias = connection.alias
|
|
34
|
-
if alias not in _UNACCENT_AVAILABLE:
|
|
35
|
-
try:
|
|
36
|
-
with connection.cursor() as cursor:
|
|
37
|
-
cursor.execute(
|
|
38
|
-
"SELECT 1 FROM pg_extension WHERE extname = 'unaccent'"
|
|
39
|
-
)
|
|
40
|
-
_UNACCENT_AVAILABLE[alias] = cursor.fetchone() is not None
|
|
41
|
-
except Exception:
|
|
42
|
-
_UNACCENT_AVAILABLE[alias] = False
|
|
43
|
-
return _UNACCENT_AVAILABLE[alias]
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
class DRFReactBySchemaSearchFilter(SearchFilter):
|
|
47
|
-
"""``SearchFilter`` with accent-insensitive matches on text fields.
|
|
48
|
-
|
|
49
|
-
Non-text fields keep DRF's default lookup: ``unaccent`` only accepts text,
|
|
50
|
-
so applying it to a date, number or boolean raises ``FieldError``.
|
|
51
|
-
|
|
52
|
-
Accent-insensitive search needs PostgreSQL with the ``unaccent`` extension
|
|
53
|
-
enabled and ``django.contrib.postgres`` in ``INSTALLED_APPS``. When either
|
|
54
|
-
is missing (or the database is not PostgreSQL), the filter silently falls
|
|
55
|
-
back to DRF's lookups, so search still works (accent-sensitive).
|
|
56
|
-
"""
|
|
57
|
-
|
|
58
|
-
# Resolved per request by ``filter_queryset``; None on direct calls.
|
|
59
|
-
_unaccent_enabled = None
|
|
60
|
-
|
|
61
|
-
def filter_queryset(self, request, queryset, view):
|
|
62
|
-
self._unaccent_enabled = unaccent_available(connections[queryset.db])
|
|
63
|
-
return super().filter_queryset(request, queryset, view)
|
|
64
|
-
|
|
65
|
-
def construct_search(self, field_name, queryset):
|
|
66
|
-
lookup = super().construct_search(field_name, queryset)
|
|
67
|
-
unaccent_enabled = self._unaccent_enabled
|
|
68
|
-
if unaccent_enabled is None:
|
|
69
|
-
unaccent_enabled = unaccent_available(connections[queryset.db])
|
|
70
|
-
if not unaccent_enabled:
|
|
71
|
-
return lookup
|
|
72
|
-
if not self._is_text_field(queryset.model, field_name):
|
|
73
|
-
return lookup
|
|
74
|
-
for standard_lookup in UNACCENT_LOOKUPS:
|
|
75
|
-
if lookup.endswith(f"__{standard_lookup}"):
|
|
76
|
-
return lookup[: -len(standard_lookup)] + f"unaccent__{standard_lookup}"
|
|
77
|
-
return lookup
|
|
78
|
-
|
|
79
|
-
@staticmethod
|
|
80
|
-
def _is_text_field(model, field_path):
|
|
81
|
-
"""Whether the terminal field in ``field_path`` is a text field."""
|
|
82
|
-
# Strip DRF prefix modifiers: ^, =, @, $
|
|
83
|
-
parts = str(field_path).lstrip("^=@$").split("__")
|
|
84
|
-
try:
|
|
85
|
-
for part in parts[:-1]:
|
|
86
|
-
model = model._meta.get_field(part).related_model
|
|
87
|
-
return isinstance(model._meta.get_field(parts[-1]), TEXT_FIELD_CLASSES)
|
|
88
|
-
except Exception:
|
|
89
|
-
# Unknown paths and non-relational lookups keep the standard lookup.
|
|
90
|
-
return False
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
class DRFReactBySchemaOrderingFilter(OrderingFilter):
|
|
94
|
-
def get_ordering(self, request, queryset, view):
|
|
95
|
-
"""
|
|
96
|
-
Ordering is set by a comma delimited ?ordering=... query parameter.
|
|
97
|
-
|
|
98
|
-
The `ordering` query parameter can be overridden by setting
|
|
99
|
-
the `ordering_param` value on the OrderingFilter or by
|
|
100
|
-
specifying an `ORDERING_PARAM` value in the API settings.
|
|
101
|
-
"""
|
|
102
|
-
params = request.query_params.get(self.ordering_param)
|
|
103
|
-
if params:
|
|
104
|
-
fields_raw = [param.strip() for param in params.split(",")]
|
|
105
|
-
fields = utils.serializer_to_model_fields(view, fields_raw)
|
|
106
|
-
if fields is not None:
|
|
107
|
-
ordering = self.remove_invalid_fields(queryset, fields, view, request)
|
|
108
|
-
if ordering:
|
|
109
|
-
return ordering
|
|
110
|
-
|
|
111
|
-
# No ordering was included, or all the ordering fields were invalid
|
|
112
|
-
return self.get_default_ordering(view)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/fields/__init__.py
RENAMED
|
File without changes
|
{drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/fields/array.py
RENAMED
|
File without changes
|
{drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/fields/boolean.py
RENAMED
|
File without changes
|
{drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/fields/date.py
RENAMED
|
File without changes
|
{drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/fields/file.py
RENAMED
|
File without changes
|
{drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/fields/geometry.py
RENAMED
|
File without changes
|
{drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/fields/number.py
RENAMED
|
File without changes
|
{drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/fields/relational.py
RENAMED
|
File without changes
|
{drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/fields/serializers.py
RENAMED
|
File without changes
|
{drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/fields/text.py
RENAMED
|
File without changes
|
{drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/management/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/migrations/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/patches/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/schema_bundle.py
RENAMED
|
File without changes
|
{drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/serializers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema/utils_cache.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema.egg-info/requires.txt
RENAMED
|
File without changes
|
{drf_react_by_schema-0.28.2 → drf_react_by_schema-0.28.3}/drf_react_by_schema.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|