drf-react-by-schema 0.29.0__tar.gz → 0.29.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 (52) hide show
  1. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.2}/PKG-INFO +1 -1
  2. drf_react_by_schema-0.29.2/drf_react_by_schema/autocomplete_viewsets.py +135 -0
  3. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.2}/drf_react_by_schema/metadata.py +105 -86
  4. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.2}/drf_react_by_schema/serializers.py +15 -4
  5. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.2}/drf_react_by_schema.egg-info/PKG-INFO +1 -1
  6. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.2}/setup.cfg +1 -1
  7. drf_react_by_schema-0.29.0/drf_react_by_schema/autocomplete_viewsets.py +0 -22
  8. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.2}/LICENSE +0 -0
  9. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.2}/MANIFEST.in +0 -0
  10. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.2}/README.rst +0 -0
  11. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.2}/drf_react_by_schema/__init__.py +0 -0
  12. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.2}/drf_react_by_schema/admin.py +0 -0
  13. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.2}/drf_react_by_schema/apps.py +0 -0
  14. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.2}/drf_react_by_schema/auth.py +0 -0
  15. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.2}/drf_react_by_schema/autocomplete_serializers.py +0 -0
  16. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.2}/drf_react_by_schema/checks.py +0 -0
  17. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.2}/drf_react_by_schema/date_utils.py +0 -0
  18. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.2}/drf_react_by_schema/fields/__init__.py +0 -0
  19. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.2}/drf_react_by_schema/fields/array.py +0 -0
  20. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.2}/drf_react_by_schema/fields/boolean.py +0 -0
  21. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.2}/drf_react_by_schema/fields/date.py +0 -0
  22. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.2}/drf_react_by_schema/fields/file.py +0 -0
  23. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.2}/drf_react_by_schema/fields/geometry.py +0 -0
  24. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.2}/drf_react_by_schema/fields/number.py +0 -0
  25. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.2}/drf_react_by_schema/fields/relational.py +0 -0
  26. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.2}/drf_react_by_schema/fields/serializers.py +0 -0
  27. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.2}/drf_react_by_schema/fields/text.py +0 -0
  28. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.2}/drf_react_by_schema/filters.py +0 -0
  29. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.2}/drf_react_by_schema/management/__init__.py +0 -0
  30. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.2}/drf_react_by_schema/management/commands/__init__.py +0 -0
  31. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.2}/drf_react_by_schema/management/commands/sync_react.py +0 -0
  32. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.2}/drf_react_by_schema/migrations/0001_initial.py +0 -0
  33. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.2}/drf_react_by_schema/migrations/__init__.py +0 -0
  34. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.2}/drf_react_by_schema/mixins.py +0 -0
  35. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.2}/drf_react_by_schema/models.py +0 -0
  36. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.2}/drf_react_by_schema/pagination.py +0 -0
  37. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.2}/drf_react_by_schema/patches/__init__.py +0 -0
  38. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.2}/drf_react_by_schema/patches/drf_serializers.py +0 -0
  39. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.2}/drf_react_by_schema/renderers.py +0 -0
  40. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.2}/drf_react_by_schema/schema_bundle.py +0 -0
  41. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.2}/drf_react_by_schema/tests.py +0 -0
  42. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.2}/drf_react_by_schema/typescript_generator.py +0 -0
  43. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.2}/drf_react_by_schema/urls.py +0 -0
  44. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.2}/drf_react_by_schema/utils.py +0 -0
  45. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.2}/drf_react_by_schema/utils_cache.py +0 -0
  46. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.2}/drf_react_by_schema/views.py +0 -0
  47. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.2}/drf_react_by_schema/viewsets.py +0 -0
  48. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.2}/drf_react_by_schema.egg-info/SOURCES.txt +0 -0
  49. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.2}/drf_react_by_schema.egg-info/dependency_links.txt +0 -0
  50. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.2}/drf_react_by_schema.egg-info/requires.txt +0 -0
  51. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.2}/drf_react_by_schema.egg-info/top_level.txt +0 -0
  52. {drf_react_by_schema-0.29.0 → drf_react_by_schema-0.29.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.29.0
3
+ Version: 0.29.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
@@ -0,0 +1,135 @@
1
+ import importlib
2
+
3
+ from django.apps import apps
4
+ from django.core.exceptions import FieldDoesNotExist
5
+ from rest_framework import serializers as drf_serializers
6
+
7
+ from . import settings, autocomplete_serializers, viewsets
8
+ from .utils import camel_to_snake, to_pascal_case
9
+
10
+ # ``settings`` is ``django.conf.settings.__dict__`` (see the package ``__init__``):
11
+ # ``override_settings`` clears that dict when it swaps ``_wrapped``, so the apps
12
+ # list is snapshotted here (as the generation loop below already implicitly is).
13
+ _APPS = tuple(settings["APPS"])
14
+
15
+
16
+ for app in _APPS:
17
+ for name, model in apps.all_models[app].items():
18
+ model_name = model.__name__
19
+ if isinstance(model, type):
20
+ viewset_name = f"{model_name}ViewSet"
21
+ if viewset_name not in globals():
22
+ generated_class = type(
23
+ viewset_name,
24
+ (viewsets.GenericAutocompleteViewSet,),
25
+ {
26
+ "model_name": model_name,
27
+ "app": app,
28
+ "serializer_class": getattr(
29
+ autocomplete_serializers, f"{model_name}Serializer"
30
+ ),
31
+ },
32
+ )
33
+ globals()[viewset_name] = generated_class
34
+
35
+
36
+ def _is_nested_field(field):
37
+ """Whether a declared serializer field exposes a related model (a relation).
38
+
39
+ Nested model serializers (FK/M2M/O2O and declared reverse relations) are
40
+ ``BaseSerializer`` instances; ``NestedSerializerMethodField`` wraps a
41
+ serializer class in ``serializer_class``. Plain scalars and ``source``-based
42
+ ``CharField``s are not relations and carry no autocomplete route.
43
+ """
44
+ return isinstance(field, drf_serializers.BaseSerializer) or hasattr(
45
+ field, "serializer_class"
46
+ )
47
+
48
+
49
+ def _related_model(serializer_model, field_name):
50
+ """The related model of ``field_name`` on ``serializer_model``, if any."""
51
+ try:
52
+ model_field = serializer_model._meta.get_field(field_name)
53
+ except FieldDoesNotExist:
54
+ return None
55
+ return getattr(model_field, "related_model", None)
56
+
57
+
58
+ def _iter_serializer_classes():
59
+ """Serializer classes whose model belongs to the configured apps.
60
+
61
+ Covers the package-generated serializers (whose forward relations are
62
+ auto-populated) and the app's own ``serializers`` module (which declares the
63
+ reverse relations rendered as nested/inline serializers).
64
+ """
65
+ from . import serializers as generated_serializers
66
+
67
+ modules = [generated_serializers]
68
+ for app in _APPS:
69
+ try:
70
+ modules.append(importlib.import_module(f"{app}.serializers"))
71
+ except ImportError:
72
+ pass
73
+
74
+ seen = set()
75
+ for module in modules:
76
+ for obj in vars(module).values():
77
+ if not isinstance(obj, type) or obj in seen:
78
+ continue
79
+ seen.add(obj)
80
+ yield obj
81
+
82
+
83
+ def generate_field_name_viewsets(serializer_classes):
84
+ """Autocomplete viewsets named after the relation field they serve.
85
+
86
+ The React client asks ``/autocomplete/<field>/`` using the serializer field
87
+ name, but the package only registers ``/autocomplete/<model>/``. A relation
88
+ field whose name differs from its related model — a reverse FK/M2M/O2O with a
89
+ custom ``related_name`` (``projetos`` for ``Projeto``), or a forward FK named
90
+ differently (``responsavel`` for ``Pessoa``) — therefore has no route and had
91
+ to be declared by hand in the consuming app.
92
+
93
+ This derives that route from the related model's autocomplete viewset, so its
94
+ ``serializer_class`` and configuration are inherited. An app-defined viewset
95
+ of the same name still wins: the app's ``autocomplete_viewsets`` module is
96
+ registered first and the router skips an already-registered basename.
97
+ """
98
+ for serializer_class in serializer_classes:
99
+ meta = getattr(serializer_class, "Meta", None)
100
+ serializer_model = getattr(meta, "model", None)
101
+ if (
102
+ serializer_model is None
103
+ or serializer_model._meta.app_label not in _APPS
104
+ ):
105
+ continue
106
+
107
+ for field_name, field in getattr(
108
+ serializer_class, "_declared_fields", {}
109
+ ).items():
110
+ if not _is_nested_field(field):
111
+ continue
112
+ related_model = _related_model(serializer_model, field_name)
113
+ if (
114
+ related_model is None
115
+ or related_model._meta.app_label not in _APPS
116
+ ):
117
+ continue
118
+ # The related model's own route already serves this name.
119
+ if camel_to_snake(related_model.__name__) == field_name:
120
+ continue
121
+
122
+ base_viewset = globals().get(f"{related_model.__name__}ViewSet")
123
+ if base_viewset is None:
124
+ continue
125
+ viewset_name = f"{to_pascal_case(field_name)}ViewSet"
126
+ if viewset_name in globals():
127
+ continue
128
+ globals()[viewset_name] = type(
129
+ viewset_name,
130
+ (base_viewset,),
131
+ {"__module__": __name__},
132
+ )
133
+
134
+
135
+ generate_field_name_viewsets(_iter_serializer_classes())
@@ -64,66 +64,19 @@ class Metadata(SimpleMetadata):
64
64
  return None
65
65
  return getter()
66
66
 
67
- def determine_metadata(self, request, view):
68
- serializer_class = self.get_schema_serializer_class(view)
69
- if serializer_class is None:
70
- metadata = super().determine_metadata(request, view)
71
- else:
72
- with _PinnedSerializerClass(view, serializer_class):
73
- metadata = super().determine_metadata(request, view)
74
-
75
- if not metadata.get("actions", {}).get("POST") or not hasattr(
76
- view, "get_serializer"
77
- ):
78
- return metadata
79
-
80
- try:
81
- serializer = (
82
- serializer_class() if serializer_class else view.get_serializer()
83
- )
84
- except (AttributeError, TypeError, ValueError):
85
- return metadata
86
-
87
- model = getattr(getattr(serializer, "Meta", None), "model", None)
88
- if not model:
89
- return metadata
90
-
91
- metadata.update(
92
- {
93
- "verbose_name": getattr(model._meta, "verbose_name", None),
94
- "verbose_name_plural": getattr(
95
- model._meta, "verbose_name_plural", None
96
- ),
97
- }
98
- )
99
-
100
- try:
101
- serializer_fields = serializer.get_fields()
102
- except (AttributeError, TypeError, ValueError):
103
- return metadata
104
-
105
- # Type derived method fields and expand nested serializers; the client
106
- # reads ``derived`` to tell them apart from editable relations.
107
- self._apply_serializer_field_info(metadata["actions"]["POST"], serializer)
108
-
109
- for serializer_field_name, serializer_field in serializer_fields.items():
110
- if serializer_field_name not in metadata["actions"]["POST"]:
111
- continue
112
-
113
- if (
114
- isinstance(serializer_field, serializers.CharField)
115
- and serializer_field.style
116
- ):
117
- metadata["actions"]["POST"][serializer_field_name][
118
- "style"
119
- ] = serializer_field.style
67
+ def _apply_model_field_info(self, field_infos, serializer, model):
68
+ """Enrich a schema's field infos with the model field metadata.
120
69
 
70
+ Applied to the top-level POST schema and, recursively, to every nested
71
+ serializer's children, so an inline formset's fields carry the same keys
72
+ (``conditional_visible``, ``ui_required``, ``model_required``, ...) as the
73
+ standalone model schema.
74
+ """
75
+ serializer_fields = serializer.get_fields()
121
76
  for field in model._meta.get_fields():
122
77
  field_name = getattr(field, "name", None)
123
78
 
124
- if not field_name or field_name not in metadata.get("actions", {}).get(
125
- "POST", {}
126
- ):
79
+ if not field_name or field_name not in field_infos:
127
80
  continue
128
81
 
129
82
  serializer_field = serializer_fields.get(field_name)
@@ -153,7 +106,7 @@ class Metadata(SimpleMetadata):
153
106
  )
154
107
 
155
108
  if is_now_default:
156
- metadata["actions"]["POST"][field_name]["model_default"] = (
109
+ field_infos[field_name]["model_default"] = (
157
110
  "{dateTimeNow}" if is_datetime else "{dateNow}"
158
111
  )
159
112
  elif default_value:
@@ -162,25 +115,25 @@ class Metadata(SimpleMetadata):
162
115
  model_instance = field.related_model.objects.get(
163
116
  pk=default_value
164
117
  )
165
- metadata["actions"]["POST"][field_name]["model_default"] = (
118
+ field_infos[field_name]["model_default"] = (
166
119
  serializer_field.to_representation(model_instance)
167
120
  )
168
121
  except field.related_model.DoesNotExist:
169
- metadata["actions"]["POST"][field_name][
122
+ field_infos[field_name][
170
123
  "model_default_error"
171
124
  ] = "Related object not found"
172
125
  pass
173
126
  else:
174
- metadata["actions"]["POST"][field_name][
127
+ field_infos[field_name][
175
128
  "model_default"
176
129
  ] = default_value
177
130
  elif serializer_field and serializer_field.default:
178
131
  if isinstance(serializer_field.default, (int, str)):
179
- metadata["actions"]["POST"][field_name][
132
+ field_infos[field_name][
180
133
  "model_default"
181
134
  ] = serializer_field.default
182
135
  elif isinstance(serializer_field.default, CurrentUserDefault):
183
- metadata["actions"]["POST"][field_name][
136
+ field_infos[field_name][
184
137
  "model_default"
185
138
  ] = "{currentUser}"
186
139
 
@@ -197,7 +150,7 @@ class Metadata(SimpleMetadata):
197
150
  }
198
151
  )
199
152
  if len(validators_regex) > 0:
200
- metadata["actions"]["POST"][field_name][
153
+ field_infos[field_name][
201
154
  "validators_regex"
202
155
  ] = validators_regex
203
156
 
@@ -206,11 +159,11 @@ class Metadata(SimpleMetadata):
206
159
  serializer_fields[field_name],
207
160
  serializers.ManyRelatedField,
208
161
  ):
209
- metadata["actions"]["POST"][field_name]["required"] = False
162
+ field_infos[field_name]["required"] = False
210
163
 
211
164
  # Add ui_required to OPTIONS:
212
165
  is_ui_required = getattr(field, "ui_required", False)
213
- metadata["actions"]["POST"][field_name]["ui_required"] = is_ui_required
166
+ field_infos[field_name]["ui_required"] = is_ui_required
214
167
 
215
168
  # Add model_required to OPTIONS:
216
169
  # Reverse relations (a declared reverse-FK/M2M/O2O nested list) never
@@ -231,19 +184,19 @@ class Metadata(SimpleMetadata):
231
184
  )
232
185
  )
233
186
  )
234
- metadata["actions"]["POST"][field_name]["model_required"] = is_required
187
+ field_infos[field_name]["model_required"] = is_required
235
188
 
236
189
  # Add pattern_format to OPTIONS:
237
190
  pattern_format = get_pattern_format(getattr(field, "pattern_format", None))
238
191
  if pattern_format:
239
- metadata["actions"]["POST"][field_name][
192
+ field_infos[field_name][
240
193
  "pattern_format"
241
194
  ] = pattern_format
242
195
 
243
196
  # Add help_text to OPTIONS:
244
197
  help_text = getattr(field, "help_text", None)
245
198
  if help_text:
246
- metadata["actions"]["POST"][field_name]["help_text"] = help_text
199
+ field_infos[field_name]["help_text"] = help_text
247
200
 
248
201
  # Add related model is editable to OPTIONS:
249
202
  # Non-editable (editable=False) model fields are never written by the
@@ -252,57 +205,57 @@ class Metadata(SimpleMetadata):
252
205
  if related_editable is not None:
253
206
  if not getattr(field, "editable", True):
254
207
  related_editable = False
255
- metadata["actions"]["POST"][field_name][
208
+ field_infos[field_name][
256
209
  "related_editable"
257
210
  ] = related_editable
258
211
 
259
212
  # Add DecimalField decimal_places in OPTIONS:
260
213
  decimal_places = getattr(field, "decimal_places", None)
261
214
  if decimal_places is not None:
262
- metadata["actions"]["POST"][field_name][
215
+ field_infos[field_name][
263
216
  "decimal_places"
264
217
  ] = decimal_places
265
218
 
266
219
  # Add DecimalField max_digits in OPTIONS:
267
220
  max_digits = getattr(field, "max_digits", None)
268
221
  if max_digits is not None:
269
- metadata["actions"]["POST"][field_name]["max_digits"] = max_digits
222
+ field_infos[field_name]["max_digits"] = max_digits
270
223
 
271
224
  # Add DecimalField is_currency in OPTIONS:
272
225
  is_currency = getattr(field, "is_currency", None)
273
226
  if is_currency is not None:
274
- metadata["actions"]["POST"][field_name]["is_currency"] = is_currency
227
+ field_infos[field_name]["is_currency"] = is_currency
275
228
 
276
229
  # Add DecimalField prefix in OPTIONS:
277
230
  prefix = getattr(field, "prefix", None)
278
231
  if prefix is not None:
279
- metadata["actions"]["POST"][field_name]["prefix"] = prefix
232
+ field_infos[field_name]["prefix"] = prefix
280
233
 
281
234
  # Add DecimalField suffix in OPTIONS:
282
235
  suffix = getattr(field, "suffix", None)
283
236
  if suffix is not None:
284
- metadata["actions"]["POST"][field_name]["suffix"] = suffix
237
+ field_infos[field_name]["suffix"] = suffix
285
238
 
286
239
  # Add DateField views in OPTIONS:
287
240
  views = getattr(field, "views", None)
288
241
  if views is not None:
289
- metadata["actions"]["POST"][field_name]["date_views"] = views
242
+ field_infos[field_name]["date_views"] = views
290
243
 
291
244
  if field_name in serializer_fields and (
292
245
  isinstance(
293
246
  serializer_fields[field_name],
294
247
  (serializers.ManyRelatedField, serializers.ListSerializer),
295
248
  )
296
- or metadata["actions"]["POST"][field_name]["type"] == "list"
249
+ or field_infos[field_name]["type"] == "list"
297
250
  ):
298
- metadata["actions"]["POST"][field_name]["many"] = True
251
+ field_infos[field_name]["many"] = True
299
252
 
300
253
  # Mirror the parent's required onto the child of list/array fields.
301
254
  # DRF's SimpleMetadata always reports the child serializer field as
302
255
  # required (e.g. a CharField child of a ListField), which is wrong
303
256
  # when the parent ArrayField itself is optional (blank=True/null=True).
304
257
  # The child "required" should follow the parent's "required".
305
- field_info = metadata["actions"]["POST"][field_name]
258
+ field_info = field_infos[field_name]
306
259
  if field_info.get("type") == "list" and isinstance(
307
260
  field_info.get("child"), dict
308
261
  ):
@@ -313,18 +266,18 @@ class Metadata(SimpleMetadata):
313
266
  serializer_fields[field_name],
314
267
  serializers.ManyRelatedField,
315
268
  ):
316
- metadata["actions"]["POST"][field_name]["required"] = False
269
+ field_infos[field_name]["required"] = False
317
270
 
318
271
  # Print multiline in OPTIONS:
319
272
  if isinstance(field, models.TextField):
320
- metadata["actions"]["POST"][field_name]["model_multiline"] = True
273
+ field_infos[field_name]["model_multiline"] = True
321
274
  # Rich-text fields store HTML and get a WYSIWYG widget.
322
275
  if getattr(field, "rich_text", False):
323
- metadata["actions"]["POST"][field_name]["model_html"] = True
276
+ field_infos[field_name]["model_html"] = True
324
277
 
325
278
  # Force slug to be read_only:
326
279
  if field_name == SLUG_FIELD_NAME:
327
- metadata["actions"]["POST"][field_name].update(
280
+ field_infos[field_name].update(
328
281
  {
329
282
  "required": False,
330
283
  "read_only": True,
@@ -334,22 +287,79 @@ class Metadata(SimpleMetadata):
334
287
  # Add max_file_size for ImageField and FileField in OPTIONS:
335
288
  max_file_size = getattr(field, "max_file_size", None)
336
289
  if max_file_size is not None:
337
- metadata["actions"]["POST"][field_name]["max_file_size"] = max_file_size
290
+ field_infos[field_name]["max_file_size"] = max_file_size
338
291
 
339
292
  # Add allowed_mime_types for ImageField and FileField in OPTIONS:
340
293
  allowed_mime_types = getattr(field, "allowed_mime_types", None)
341
294
  if allowed_mime_types is not None:
342
- metadata["actions"]["POST"][field_name][
295
+ field_infos[field_name][
343
296
  "allowed_mime_types"
344
297
  ] = allowed_mime_types
345
298
 
346
299
  # Add help_text to OPTIONS:
347
300
  conditional_visible = getattr(field, "conditional_visible", None)
348
301
  if conditional_visible:
349
- metadata["actions"]["POST"][field_name][
302
+ field_infos[field_name][
350
303
  "conditional_visible"
351
304
  ] = conditional_visible
352
305
 
306
+
307
+ def determine_metadata(self, request, view):
308
+ serializer_class = self.get_schema_serializer_class(view)
309
+ if serializer_class is None:
310
+ metadata = super().determine_metadata(request, view)
311
+ else:
312
+ with _PinnedSerializerClass(view, serializer_class):
313
+ metadata = super().determine_metadata(request, view)
314
+
315
+ if not metadata.get("actions", {}).get("POST") or not hasattr(
316
+ view, "get_serializer"
317
+ ):
318
+ return metadata
319
+
320
+ try:
321
+ serializer = (
322
+ serializer_class() if serializer_class else view.get_serializer()
323
+ )
324
+ except (AttributeError, TypeError, ValueError):
325
+ return metadata
326
+
327
+ model = getattr(getattr(serializer, "Meta", None), "model", None)
328
+ if not model:
329
+ return metadata
330
+
331
+ metadata.update(
332
+ {
333
+ "verbose_name": getattr(model._meta, "verbose_name", None),
334
+ "verbose_name_plural": getattr(
335
+ model._meta, "verbose_name_plural", None
336
+ ),
337
+ }
338
+ )
339
+
340
+ try:
341
+ serializer_fields = serializer.get_fields()
342
+ except (AttributeError, TypeError, ValueError):
343
+ return metadata
344
+
345
+ # Type derived method fields and expand nested serializers; the client
346
+ # reads ``derived`` to tell them apart from editable relations.
347
+ self._apply_serializer_field_info(metadata["actions"]["POST"], serializer)
348
+
349
+ for serializer_field_name, serializer_field in serializer_fields.items():
350
+ if serializer_field_name not in metadata["actions"]["POST"]:
351
+ continue
352
+
353
+ if (
354
+ isinstance(serializer_field, serializers.CharField)
355
+ and serializer_field.style
356
+ ):
357
+ metadata["actions"]["POST"][serializer_field_name][
358
+ "style"
359
+ ] = serializer_field.style
360
+
361
+ self._apply_model_field_info(metadata["actions"]["POST"], serializer, model)
362
+
353
363
  # PostGIS geometry fields have no DRF type that SimpleMetadata can emit,
354
364
  # so set the drf-react-by-schema geometry types directly. Only applied
355
365
  # when the database engine is PostGIS.
@@ -412,9 +422,18 @@ class Metadata(SimpleMetadata):
412
422
  self._apply_inline_object_info(info, field, info["children"])
413
423
 
414
424
  def _nested_serializer_info(self, serializer, parent_model=None):
415
- """Schema info for a nested serializer, with its derived fields typed."""
425
+ """Schema info for a nested serializer, with its derived fields typed.
426
+
427
+ The model-field enrichment (``conditional_visible``, ``ui_required``,
428
+ ``model_required``, ...) is applied here too, so an inline formset's child
429
+ fields carry the same keys as the standalone model schema — the widget
430
+ reads ``conditional_visible`` from these nested children.
431
+ """
416
432
  info = self.get_serializer_info(serializer)
417
433
  self._apply_serializer_field_info(info, serializer)
434
+ nested_model = getattr(getattr(serializer, "Meta", None), "model", None)
435
+ if nested_model is not None:
436
+ self._apply_model_field_info(info, serializer, nested_model)
418
437
  if parent_model is not None:
419
438
  self._mark_auto_managed(info, serializer, parent_model)
420
439
  return info
@@ -600,15 +600,26 @@ class GenericSerializer(
600
600
  Binding the row's existing instance for the duration of the validation
601
601
  lets DRF's validators exclude it — the write itself still applies the
602
602
  implicit diff (see ``utils.update_nested_relations``).
603
+
604
+ A row that targets an existing object is also a *partial* write: the
605
+ client sends only the fields it changed (an upload-first file is omitted
606
+ when it is unchanged), so a field absent from the payload keeps its stored
607
+ value and must be skipped, not required. DRF reads ``partial`` from the
608
+ root serializer (``Field.validate_empty_values``), so it is toggled there
609
+ for the duration of this row's validation.
603
610
  """
604
611
  nested_instance = self._nested_row_instance(data)
605
- if nested_instance is not None:
606
- self.instance = nested_instance
612
+ if nested_instance is None:
613
+ return super().run_validation(data)
614
+ self.instance = nested_instance
615
+ root = self.root
616
+ was_partial = root.partial
617
+ root.partial = True
607
618
  try:
608
619
  return super().run_validation(data)
609
620
  finally:
610
- if nested_instance is not None:
611
- self.instance = None
621
+ root.partial = was_partial
622
+ self.instance = None
612
623
 
613
624
  def _nested_write_context(self):
614
625
  """``(parent_serializer, back_fk)`` when validating a nested-write row.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: drf-react-by-schema
3
- Version: 0.29.0
3
+ Version: 0.29.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
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = drf_react_by_schema
3
- version = 0.29.0
3
+ version = 0.29.2
4
4
  description = A Django app to easily integrate with React from models.
5
5
  long_description = file: README.rst
6
6
  long_description_content_type = text/x-rst
@@ -1,22 +0,0 @@
1
- from django.apps import apps
2
-
3
- from . import settings, autocomplete_serializers, viewsets
4
-
5
- for app in settings["APPS"]:
6
- for name, model in apps.all_models[app].items():
7
- model_name = model.__name__
8
- if isinstance(model, type):
9
- viewset_name = f"{model_name}ViewSet"
10
- if viewset_name not in globals():
11
- generated_class = type(
12
- viewset_name,
13
- (viewsets.GenericAutocompleteViewSet,),
14
- {
15
- "model_name": model_name,
16
- "app": app,
17
- "serializer_class": getattr(
18
- autocomplete_serializers, f"{model_name}Serializer"
19
- ),
20
- },
21
- )
22
- globals()[viewset_name] = generated_class