drf-react-by-schema 0.28.1__tar.gz → 0.28.2__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.
Files changed (51) hide show
  1. {drf_react_by_schema-0.28.1 → drf_react_by_schema-0.28.2}/PKG-INFO +38 -1
  2. {drf_react_by_schema-0.28.1 → drf_react_by_schema-0.28.2}/README.rst +37 -0
  3. drf_react_by_schema-0.28.2/drf_react_by_schema/filters.py +112 -0
  4. {drf_react_by_schema-0.28.1 → drf_react_by_schema-0.28.2}/drf_react_by_schema/schema_bundle.py +6 -1
  5. {drf_react_by_schema-0.28.1 → drf_react_by_schema-0.28.2}/drf_react_by_schema/utils.py +11 -0
  6. {drf_react_by_schema-0.28.1 → drf_react_by_schema-0.28.2}/drf_react_by_schema/viewsets.py +8 -2
  7. {drf_react_by_schema-0.28.1 → drf_react_by_schema-0.28.2}/drf_react_by_schema.egg-info/PKG-INFO +38 -1
  8. {drf_react_by_schema-0.28.1 → drf_react_by_schema-0.28.2}/setup.cfg +1 -1
  9. drf_react_by_schema-0.28.1/drf_react_by_schema/filters.py +0 -24
  10. {drf_react_by_schema-0.28.1 → drf_react_by_schema-0.28.2}/LICENSE +0 -0
  11. {drf_react_by_schema-0.28.1 → drf_react_by_schema-0.28.2}/MANIFEST.in +0 -0
  12. {drf_react_by_schema-0.28.1 → drf_react_by_schema-0.28.2}/drf_react_by_schema/__init__.py +0 -0
  13. {drf_react_by_schema-0.28.1 → drf_react_by_schema-0.28.2}/drf_react_by_schema/admin.py +0 -0
  14. {drf_react_by_schema-0.28.1 → drf_react_by_schema-0.28.2}/drf_react_by_schema/apps.py +0 -0
  15. {drf_react_by_schema-0.28.1 → drf_react_by_schema-0.28.2}/drf_react_by_schema/auth.py +0 -0
  16. {drf_react_by_schema-0.28.1 → drf_react_by_schema-0.28.2}/drf_react_by_schema/autocomplete_serializers.py +0 -0
  17. {drf_react_by_schema-0.28.1 → drf_react_by_schema-0.28.2}/drf_react_by_schema/autocomplete_viewsets.py +0 -0
  18. {drf_react_by_schema-0.28.1 → drf_react_by_schema-0.28.2}/drf_react_by_schema/checks.py +0 -0
  19. {drf_react_by_schema-0.28.1 → drf_react_by_schema-0.28.2}/drf_react_by_schema/date_utils.py +0 -0
  20. {drf_react_by_schema-0.28.1 → drf_react_by_schema-0.28.2}/drf_react_by_schema/fields/__init__.py +0 -0
  21. {drf_react_by_schema-0.28.1 → drf_react_by_schema-0.28.2}/drf_react_by_schema/fields/array.py +0 -0
  22. {drf_react_by_schema-0.28.1 → drf_react_by_schema-0.28.2}/drf_react_by_schema/fields/boolean.py +0 -0
  23. {drf_react_by_schema-0.28.1 → drf_react_by_schema-0.28.2}/drf_react_by_schema/fields/date.py +0 -0
  24. {drf_react_by_schema-0.28.1 → drf_react_by_schema-0.28.2}/drf_react_by_schema/fields/file.py +0 -0
  25. {drf_react_by_schema-0.28.1 → drf_react_by_schema-0.28.2}/drf_react_by_schema/fields/geometry.py +0 -0
  26. {drf_react_by_schema-0.28.1 → drf_react_by_schema-0.28.2}/drf_react_by_schema/fields/number.py +0 -0
  27. {drf_react_by_schema-0.28.1 → drf_react_by_schema-0.28.2}/drf_react_by_schema/fields/relational.py +0 -0
  28. {drf_react_by_schema-0.28.1 → drf_react_by_schema-0.28.2}/drf_react_by_schema/fields/serializers.py +0 -0
  29. {drf_react_by_schema-0.28.1 → drf_react_by_schema-0.28.2}/drf_react_by_schema/fields/text.py +0 -0
  30. {drf_react_by_schema-0.28.1 → drf_react_by_schema-0.28.2}/drf_react_by_schema/management/__init__.py +0 -0
  31. {drf_react_by_schema-0.28.1 → drf_react_by_schema-0.28.2}/drf_react_by_schema/management/commands/__init__.py +0 -0
  32. {drf_react_by_schema-0.28.1 → drf_react_by_schema-0.28.2}/drf_react_by_schema/management/commands/sync_react.py +0 -0
  33. {drf_react_by_schema-0.28.1 → drf_react_by_schema-0.28.2}/drf_react_by_schema/metadata.py +0 -0
  34. {drf_react_by_schema-0.28.1 → drf_react_by_schema-0.28.2}/drf_react_by_schema/migrations/__init__.py +0 -0
  35. {drf_react_by_schema-0.28.1 → drf_react_by_schema-0.28.2}/drf_react_by_schema/mixins.py +0 -0
  36. {drf_react_by_schema-0.28.1 → drf_react_by_schema-0.28.2}/drf_react_by_schema/models.py +0 -0
  37. {drf_react_by_schema-0.28.1 → drf_react_by_schema-0.28.2}/drf_react_by_schema/pagination.py +0 -0
  38. {drf_react_by_schema-0.28.1 → drf_react_by_schema-0.28.2}/drf_react_by_schema/patches/__init__.py +0 -0
  39. {drf_react_by_schema-0.28.1 → drf_react_by_schema-0.28.2}/drf_react_by_schema/patches/drf_serializers.py +0 -0
  40. {drf_react_by_schema-0.28.1 → drf_react_by_schema-0.28.2}/drf_react_by_schema/renderers.py +0 -0
  41. {drf_react_by_schema-0.28.1 → drf_react_by_schema-0.28.2}/drf_react_by_schema/serializers.py +0 -0
  42. {drf_react_by_schema-0.28.1 → drf_react_by_schema-0.28.2}/drf_react_by_schema/tests.py +0 -0
  43. {drf_react_by_schema-0.28.1 → drf_react_by_schema-0.28.2}/drf_react_by_schema/typescript_generator.py +0 -0
  44. {drf_react_by_schema-0.28.1 → drf_react_by_schema-0.28.2}/drf_react_by_schema/urls.py +0 -0
  45. {drf_react_by_schema-0.28.1 → drf_react_by_schema-0.28.2}/drf_react_by_schema/utils_cache.py +0 -0
  46. {drf_react_by_schema-0.28.1 → drf_react_by_schema-0.28.2}/drf_react_by_schema/views.py +0 -0
  47. {drf_react_by_schema-0.28.1 → drf_react_by_schema-0.28.2}/drf_react_by_schema.egg-info/SOURCES.txt +0 -0
  48. {drf_react_by_schema-0.28.1 → drf_react_by_schema-0.28.2}/drf_react_by_schema.egg-info/dependency_links.txt +0 -0
  49. {drf_react_by_schema-0.28.1 → drf_react_by_schema-0.28.2}/drf_react_by_schema.egg-info/requires.txt +0 -0
  50. {drf_react_by_schema-0.28.1 → drf_react_by_schema-0.28.2}/drf_react_by_schema.egg-info/top_level.txt +0 -0
  51. {drf_react_by_schema-0.28.1 → drf_react_by_schema-0.28.2}/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.1
3
+ Version: 0.28.2
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
@@ -115,6 +115,43 @@ single release line. The supported combinations are exercised in CI (see
115
115
  drf-react-by-schema
116
116
  drf-excel<2.6
117
117
 
118
+ Django 3.2 is kept as **legacy support**: the 3.2-only pin above and its CI job
119
+ will be dropped when the supported floor moves to 4.2. New code should target
120
+ 4.2-5.2; Django-3.2-only branches must stay isolated so the migration is a single
121
+ clean-up pass.
122
+
123
+ Accent-insensitive search
124
+ -------------------------
125
+ On PostgreSQL with the ``unaccent`` extension enabled (and
126
+ ``django.contrib.postgres`` in ``INSTALLED_APPS``), replace DRF's search filter
127
+ with the package's::
128
+
129
+ REST_FRAMEWORK = {
130
+ "DEFAULT_FILTER_BACKENDS": [
131
+ "drf_react_by_schema.filters.DRFReactBySchemaSearchFilter",
132
+ "drf_react_by_schema.filters.DRFReactBySchemaOrderingFilter",
133
+ ],
134
+ }
135
+
136
+ ``unaccent`` is applied only to text fields; dates, numbers and booleans keep
137
+ DRF's default lookup, because ``unaccent`` raises on them.
138
+
139
+ The filter is **opt-in**: apps that keep DRF's ``SearchFilter`` are unaffected
140
+ and do not need the extension. It probes the backend, ``INSTALLED_APPS`` and the
141
+ database extension once per connection alias. If the extension is missing — or
142
+ the database is not PostgreSQL, or ``django.contrib.postgres`` is not installed
143
+ — it falls back to DRF's normal lookups: search keeps working (accent-sensitive)
144
+ instead of raising ``FieldError``. Call
145
+ ``drf_react_by_schema.filters.reset_unaccent_cache()`` if the extension is
146
+ installed while the process is running.
147
+
148
+ To create the extension, add a migration in the consuming app::
149
+
150
+ from django.contrib.postgres.operations import UnaccentExtension
151
+
152
+ class Migration(migrations.Migration):
153
+ operations = [UnaccentExtension()]
154
+
118
155
  Optional Dependencies
119
156
  ---------------------
120
157
  For enhanced caching capabilities in the automatically generated viewsets, you can install optional dependencies:
@@ -83,6 +83,43 @@ single release line. The supported combinations are exercised in CI (see
83
83
  drf-react-by-schema
84
84
  drf-excel<2.6
85
85
 
86
+ Django 3.2 is kept as **legacy support**: the 3.2-only pin above and its CI job
87
+ will be dropped when the supported floor moves to 4.2. New code should target
88
+ 4.2-5.2; Django-3.2-only branches must stay isolated so the migration is a single
89
+ clean-up pass.
90
+
91
+ Accent-insensitive search
92
+ -------------------------
93
+ On PostgreSQL with the ``unaccent`` extension enabled (and
94
+ ``django.contrib.postgres`` in ``INSTALLED_APPS``), replace DRF's search filter
95
+ with the package's::
96
+
97
+ REST_FRAMEWORK = {
98
+ "DEFAULT_FILTER_BACKENDS": [
99
+ "drf_react_by_schema.filters.DRFReactBySchemaSearchFilter",
100
+ "drf_react_by_schema.filters.DRFReactBySchemaOrderingFilter",
101
+ ],
102
+ }
103
+
104
+ ``unaccent`` is applied only to text fields; dates, numbers and booleans keep
105
+ DRF's default lookup, because ``unaccent`` raises on them.
106
+
107
+ The filter is **opt-in**: apps that keep DRF's ``SearchFilter`` are unaffected
108
+ and do not need the extension. It probes the backend, ``INSTALLED_APPS`` and the
109
+ database extension once per connection alias. If the extension is missing — or
110
+ the database is not PostgreSQL, or ``django.contrib.postgres`` is not installed
111
+ — it falls back to DRF's normal lookups: search keeps working (accent-sensitive)
112
+ instead of raising ``FieldError``. Call
113
+ ``drf_react_by_schema.filters.reset_unaccent_cache()`` if the extension is
114
+ installed while the process is running.
115
+
116
+ To create the extension, add a migration in the consuming app::
117
+
118
+ from django.contrib.postgres.operations import UnaccentExtension
119
+
120
+ class Migration(migrations.Migration):
121
+ operations = [UnaccentExtension()]
122
+
86
123
  Optional Dependencies
87
124
  ---------------------
88
125
  For enhanced caching capabilities in the automatically generated viewsets, you can install optional dependencies:
@@ -0,0 +1,112 @@
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)
@@ -53,6 +53,7 @@ from .utils import (
53
53
  DATE_FILTER_OPERATORS,
54
54
  GRID_FILTER_OPERATORS,
55
55
  camel_to_snake,
56
+ is_searchable_model_field,
56
57
  sum_rows_capabilities,
57
58
  )
58
59
 
@@ -543,7 +544,11 @@ def _search_ordering_fields(view_instance) -> tuple[list, list]:
543
544
  except Exception:
544
545
  return [], []
545
546
  return (
546
- [str(field) for field in (view_instance.search_fields or [])],
547
+ [
548
+ str(field)
549
+ for field in (view_instance.search_fields or [])
550
+ if is_searchable_model_field(view_instance, str(field))
551
+ ],
547
552
  [str(field) for field in (view_instance.ordering_fields or [])],
548
553
  )
549
554
 
@@ -332,6 +332,17 @@ def _resolve_model_field(view, path):
332
332
  return field
333
333
 
334
334
 
335
+ def is_searchable_model_field(view, path):
336
+ """Whether ``path`` accepts DRF's default ``icontains`` search lookup.
337
+
338
+ A path ending on a relation (FK, reverse or m2m) has no ``icontains``
339
+ lookup: putting it in ``search_fields`` makes every ``?search=`` request
340
+ raise ``FieldError``. Unknown paths (e.g. annotations) are left untouched.
341
+ """
342
+ field = _resolve_model_field(view, path)
343
+ return field is None or not field.is_relation
344
+
345
+
335
346
  def _is_non_string_field(view, target_field, column_field_serializer):
336
347
  """Whether the grid filter must treat the target as non-string.
337
348
 
@@ -550,8 +550,14 @@ class DRFReactBySchemaBaseModelViewSet(
550
550
  self, list(serializer_fields.keys())
551
551
  )
552
552
 
553
- if not self.search_fields:
554
- self.search_fields = target_fields
553
+ if not self.search_fields and target_fields:
554
+ # Relation-terminated paths have no ``icontains`` lookup; DRF
555
+ # would raise on every ``?search=``. Ordering them is valid.
556
+ self.search_fields = [
557
+ target_field
558
+ for target_field in target_fields
559
+ if utils.is_searchable_model_field(self, target_field)
560
+ ]
555
561
  if not self.ordering_fields:
556
562
  self.ordering_fields = target_fields
557
563
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: drf-react-by-schema
3
- Version: 0.28.1
3
+ Version: 0.28.2
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
@@ -115,6 +115,43 @@ single release line. The supported combinations are exercised in CI (see
115
115
  drf-react-by-schema
116
116
  drf-excel<2.6
117
117
 
118
+ Django 3.2 is kept as **legacy support**: the 3.2-only pin above and its CI job
119
+ will be dropped when the supported floor moves to 4.2. New code should target
120
+ 4.2-5.2; Django-3.2-only branches must stay isolated so the migration is a single
121
+ clean-up pass.
122
+
123
+ Accent-insensitive search
124
+ -------------------------
125
+ On PostgreSQL with the ``unaccent`` extension enabled (and
126
+ ``django.contrib.postgres`` in ``INSTALLED_APPS``), replace DRF's search filter
127
+ with the package's::
128
+
129
+ REST_FRAMEWORK = {
130
+ "DEFAULT_FILTER_BACKENDS": [
131
+ "drf_react_by_schema.filters.DRFReactBySchemaSearchFilter",
132
+ "drf_react_by_schema.filters.DRFReactBySchemaOrderingFilter",
133
+ ],
134
+ }
135
+
136
+ ``unaccent`` is applied only to text fields; dates, numbers and booleans keep
137
+ DRF's default lookup, because ``unaccent`` raises on them.
138
+
139
+ The filter is **opt-in**: apps that keep DRF's ``SearchFilter`` are unaffected
140
+ and do not need the extension. It probes the backend, ``INSTALLED_APPS`` and the
141
+ database extension once per connection alias. If the extension is missing — or
142
+ the database is not PostgreSQL, or ``django.contrib.postgres`` is not installed
143
+ — it falls back to DRF's normal lookups: search keeps working (accent-sensitive)
144
+ instead of raising ``FieldError``. Call
145
+ ``drf_react_by_schema.filters.reset_unaccent_cache()`` if the extension is
146
+ installed while the process is running.
147
+
148
+ To create the extension, add a migration in the consuming app::
149
+
150
+ from django.contrib.postgres.operations import UnaccentExtension
151
+
152
+ class Migration(migrations.Migration):
153
+ operations = [UnaccentExtension()]
154
+
118
155
  Optional Dependencies
119
156
  ---------------------
120
157
  For enhanced caching capabilities in the automatically generated viewsets, you can install optional dependencies:
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = drf_react_by_schema
3
- version = 0.28.1
3
+ version = 0.28.2
4
4
  description = A Django app to easily integrate with React from models.
5
5
  long_description = file: README.rst
6
6
  url = https://gitlab.com/eita/drf-react-by-schema/drf-react-by-schema-django
@@ -1,24 +0,0 @@
1
- from rest_framework.filters import OrderingFilter, SearchFilter
2
- from . import utils
3
-
4
-
5
- class DRFReactBySchemaOrderingFilter(OrderingFilter):
6
- def get_ordering(self, request, queryset, view):
7
- """
8
- Ordering is set by a comma delimited ?ordering=... query parameter.
9
-
10
- The `ordering` query parameter can be overridden by setting
11
- the `ordering_param` value on the OrderingFilter or by
12
- specifying an `ORDERING_PARAM` value in the API settings.
13
- """
14
- params = request.query_params.get(self.ordering_param)
15
- if params:
16
- fields_raw = [param.strip() for param in params.split(",")]
17
- fields = utils.serializer_to_model_fields(view, fields_raw)
18
- if fields is not None:
19
- ordering = self.remove_invalid_fields(queryset, fields, view, request)
20
- if ordering:
21
- return ordering
22
-
23
- # No ordering was included, or all the ordering fields were invalid
24
- return self.get_default_ordering(view)