drf-react-by-schema 0.26.0__tar.gz → 0.26.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 (44) hide show
  1. {drf_react_by_schema-0.26.0 → drf_react_by_schema-0.26.2}/PKG-INFO +1 -1
  2. drf_react_by_schema-0.26.2/drf_react_by_schema/management/commands/__init__.py +0 -0
  3. drf_react_by_schema-0.26.2/drf_react_by_schema/management/commands/sync_react.py +318 -0
  4. {drf_react_by_schema-0.26.0 → drf_react_by_schema-0.26.2}/drf_react_by_schema/metadata.py +8 -3
  5. drf_react_by_schema-0.26.2/drf_react_by_schema/migrations/__init__.py +0 -0
  6. {drf_react_by_schema-0.26.0 → drf_react_by_schema-0.26.2}/drf_react_by_schema/serializers.py +4 -2
  7. {drf_react_by_schema-0.26.0 → drf_react_by_schema-0.26.2}/drf_react_by_schema.egg-info/PKG-INFO +1 -1
  8. {drf_react_by_schema-0.26.0 → drf_react_by_schema-0.26.2}/drf_react_by_schema.egg-info/SOURCES.txt +3 -0
  9. {drf_react_by_schema-0.26.0 → drf_react_by_schema-0.26.2}/setup.cfg +1 -1
  10. {drf_react_by_schema-0.26.0 → drf_react_by_schema-0.26.2}/LICENSE +0 -0
  11. {drf_react_by_schema-0.26.0 → drf_react_by_schema-0.26.2}/MANIFEST.in +0 -0
  12. {drf_react_by_schema-0.26.0 → drf_react_by_schema-0.26.2}/README.rst +0 -0
  13. {drf_react_by_schema-0.26.0 → drf_react_by_schema-0.26.2}/drf_react_by_schema/__init__.py +0 -0
  14. {drf_react_by_schema-0.26.0 → drf_react_by_schema-0.26.2}/drf_react_by_schema/admin.py +0 -0
  15. {drf_react_by_schema-0.26.0 → drf_react_by_schema-0.26.2}/drf_react_by_schema/apps.py +0 -0
  16. {drf_react_by_schema-0.26.0 → drf_react_by_schema-0.26.2}/drf_react_by_schema/autocomplete_serializers.py +0 -0
  17. {drf_react_by_schema-0.26.0 → drf_react_by_schema-0.26.2}/drf_react_by_schema/autocomplete_viewsets.py +0 -0
  18. {drf_react_by_schema-0.26.0 → drf_react_by_schema-0.26.2}/drf_react_by_schema/fields/__init__.py +0 -0
  19. {drf_react_by_schema-0.26.0 → drf_react_by_schema-0.26.2}/drf_react_by_schema/fields/array.py +0 -0
  20. {drf_react_by_schema-0.26.0 → drf_react_by_schema-0.26.2}/drf_react_by_schema/fields/boolean.py +0 -0
  21. {drf_react_by_schema-0.26.0 → drf_react_by_schema-0.26.2}/drf_react_by_schema/fields/date.py +0 -0
  22. {drf_react_by_schema-0.26.0 → drf_react_by_schema-0.26.2}/drf_react_by_schema/fields/file.py +0 -0
  23. {drf_react_by_schema-0.26.0 → drf_react_by_schema-0.26.2}/drf_react_by_schema/fields/number.py +0 -0
  24. {drf_react_by_schema-0.26.0 → drf_react_by_schema-0.26.2}/drf_react_by_schema/fields/relational.py +0 -0
  25. {drf_react_by_schema-0.26.0 → drf_react_by_schema-0.26.2}/drf_react_by_schema/fields/serializers.py +0 -0
  26. {drf_react_by_schema-0.26.0 → drf_react_by_schema-0.26.2}/drf_react_by_schema/fields/text.py +0 -0
  27. {drf_react_by_schema-0.26.0 → drf_react_by_schema-0.26.2}/drf_react_by_schema/filters.py +0 -0
  28. {drf_react_by_schema-0.26.0/drf_react_by_schema/migrations → drf_react_by_schema-0.26.2/drf_react_by_schema/management}/__init__.py +0 -0
  29. {drf_react_by_schema-0.26.0 → drf_react_by_schema-0.26.2}/drf_react_by_schema/mixins.py +0 -0
  30. {drf_react_by_schema-0.26.0 → drf_react_by_schema-0.26.2}/drf_react_by_schema/models.py +0 -0
  31. {drf_react_by_schema-0.26.0 → drf_react_by_schema-0.26.2}/drf_react_by_schema/pagination.py +0 -0
  32. {drf_react_by_schema-0.26.0 → drf_react_by_schema-0.26.2}/drf_react_by_schema/patches/__init__.py +0 -0
  33. {drf_react_by_schema-0.26.0 → drf_react_by_schema-0.26.2}/drf_react_by_schema/patches/drf_serializers.py +0 -0
  34. {drf_react_by_schema-0.26.0 → drf_react_by_schema-0.26.2}/drf_react_by_schema/renderers.py +0 -0
  35. {drf_react_by_schema-0.26.0 → drf_react_by_schema-0.26.2}/drf_react_by_schema/tests.py +0 -0
  36. {drf_react_by_schema-0.26.0 → drf_react_by_schema-0.26.2}/drf_react_by_schema/typescript_generator.py +0 -0
  37. {drf_react_by_schema-0.26.0 → drf_react_by_schema-0.26.2}/drf_react_by_schema/urls.py +0 -0
  38. {drf_react_by_schema-0.26.0 → drf_react_by_schema-0.26.2}/drf_react_by_schema/utils.py +0 -0
  39. {drf_react_by_schema-0.26.0 → drf_react_by_schema-0.26.2}/drf_react_by_schema/utils_cache.py +0 -0
  40. {drf_react_by_schema-0.26.0 → drf_react_by_schema-0.26.2}/drf_react_by_schema/viewsets.py +0 -0
  41. {drf_react_by_schema-0.26.0 → drf_react_by_schema-0.26.2}/drf_react_by_schema.egg-info/dependency_links.txt +0 -0
  42. {drf_react_by_schema-0.26.0 → drf_react_by_schema-0.26.2}/drf_react_by_schema.egg-info/requires.txt +0 -0
  43. {drf_react_by_schema-0.26.0 → drf_react_by_schema-0.26.2}/drf_react_by_schema.egg-info/top_level.txt +0 -0
  44. {drf_react_by_schema-0.26.0 → drf_react_by_schema-0.26.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.26.0
3
+ Version: 0.26.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,318 @@
1
+ import os
2
+ import json
3
+ import shutil
4
+ import importlib
5
+ import traceback
6
+ from django.conf import settings
7
+ from django.core.management.base import BaseCommand
8
+ from django.core.serializers.json import DjangoJSONEncoder
9
+ from django.utils.encoding import force_str
10
+ from django.apps import apps
11
+ from rest_framework.test import APIRequestFactory
12
+ from rest_framework.request import Request
13
+
14
+ from drf_react_by_schema import settings as package_settings
15
+ from drf_react_by_schema.metadata import Metadata
16
+ from drf_react_by_schema.typescript_generator import TypeScriptGenerator
17
+ from drf_react_by_schema import viewsets as drf_viewsets
18
+
19
+
20
+ class Command(BaseCommand):
21
+ help = "Syncs Django models to React schemas and types"
22
+
23
+ def handle(self, *args, **kwargs):
24
+ drf_settings = getattr(settings, "DRF_REACT_BY_SCHEMA", {})
25
+ base_dir = os.path.join(
26
+ settings.BASE_DIR,
27
+ drf_settings.get(
28
+ "REACT_APP_DIR",
29
+ "frontend",
30
+ ),
31
+ )
32
+
33
+ output_dir = os.path.join(base_dir, "src/drfReactBySchema")
34
+ schema_path = os.path.join(output_dir, "schemas")
35
+
36
+ if os.path.exists(output_dir):
37
+ self.stdout.write(f"Cleaning output directory: {output_dir}")
38
+ shutil.rmtree(output_dir)
39
+
40
+ os.makedirs(schema_path, exist_ok=True)
41
+
42
+ apps_to_sync = drf_settings.get("APPS", [])
43
+
44
+ self.stdout.write(f"Syncing apps: {apps_to_sync}")
45
+
46
+ index_exports = ["import { SchemaLoader } from 'drf-react-by-schema';"]
47
+
48
+ factory = APIRequestFactory()
49
+ wsgi_request = factory.options("/")
50
+ request = Request(wsgi_request)
51
+
52
+ modules_to_sync = []
53
+ for app_label in apps_to_sync:
54
+ modules_to_sync.append(f"{app_label}.viewsets")
55
+ modules_to_sync.append("drf_react_by_schema.viewsets")
56
+
57
+ processed_models = set()
58
+ global_ts_gen = TypeScriptGenerator()
59
+ all_interfaces = []
60
+
61
+ for module_str in modules_to_sync:
62
+ try:
63
+ module = importlib.import_module(module_str)
64
+ module_name = module_str.split(".")[0]
65
+ self.stdout.write(f"Processing module: {module_name}...")
66
+ for viewset_name, viewset in module.__dict__.items():
67
+ if isinstance(viewset, type) and viewset_name not in [
68
+ "DRFReactBySchemaBaseModelViewSet",
69
+ "GenericRelatedModelViewSet",
70
+ "GenericAutocompleteViewSet",
71
+ "GenericViewSet",
72
+ "LoggingMixin",
73
+ "generated_nested_class",
74
+ "generated_class",
75
+ ]:
76
+ for parent in viewset.mro():
77
+ if "ViewSet" in parent.__name__:
78
+ # Subclass to override properties that might be read-only on the base
79
+ class MockViewSet(viewset):
80
+ @property
81
+ def allowed_methods(self):
82
+ return [
83
+ "GET",
84
+ "POST",
85
+ "PUT",
86
+ "PATCH",
87
+ "DELETE",
88
+ "OPTIONS",
89
+ ]
90
+
91
+ def get_object(self):
92
+ # Avoid DB hit for metadata generation
93
+ if hasattr(self, "model") and self.model:
94
+ return self.model()
95
+ return None
96
+
97
+ view_instance = MockViewSet()
98
+ view_instance.request = request
99
+ view_instance.format_kwarg = None
100
+ view_instance.kwargs = {"pk": 1}
101
+ view_instance.action = "create" # Ensure get_serializer implementation has a context
102
+
103
+ # Mock permissions
104
+ view_instance.check_permissions = lambda req: None
105
+ view_instance.check_object_permissions = (
106
+ lambda req, obj: None
107
+ )
108
+
109
+ # Check if it is a valid viewset to process
110
+ if (
111
+ not getattr(view_instance, "model", None)
112
+ and not getattr(view_instance, "queryset", None)
113
+ and viewset_name != "EndpointsViewSet"
114
+ ):
115
+ continue
116
+
117
+ model_name = viewset_name.removesuffix("ViewSet")
118
+ if model_name in processed_models:
119
+ continue
120
+ processed_models.add(model_name)
121
+
122
+ # A. Generate Schema
123
+ meta_tool = Metadata()
124
+ raw_metadata = meta_tool.determine_metadata(
125
+ request, view_instance
126
+ )
127
+
128
+ simplified_schema = {
129
+ "schema": raw_metadata.get("actions", {}).get(
130
+ "POST", {}
131
+ ),
132
+ "verbose_name": raw_metadata.get(
133
+ "verbose_name", ""
134
+ ),
135
+ "verbose_name_plural": raw_metadata.get(
136
+ "verbose_name_plural", ""
137
+ ),
138
+ }
139
+
140
+ # We want to export it as json
141
+ schema_var_name = f"{model_name}Schema"
142
+ json_filename = f"{schema_var_name}.json"
143
+ with open(
144
+ os.path.join(schema_path, json_filename), "w"
145
+ ) as f:
146
+ json.dump(
147
+ simplified_schema,
148
+ f,
149
+ cls=DjangoJSONEncoder,
150
+ indent=2,
151
+ )
152
+
153
+ index_exports.append(
154
+ f"export const {schema_var_name} = (() => import('./schemas/{json_filename}')) as SchemaLoader;"
155
+ )
156
+
157
+ # B. Generate TypeScript
158
+ interface_code = global_ts_gen.generate_interface(
159
+ model_name, view_instance
160
+ )
161
+ if interface_code:
162
+ all_interfaces.append(interface_code)
163
+ break
164
+ except ImportError:
165
+ pass
166
+ except Exception as e:
167
+ print(f"--- Error loading module: {module_str} ---")
168
+ traceback.print_exc()
169
+
170
+ # Generate Imports for Types
171
+ imports = [
172
+ "import { ChoiceItem } from 'drf-react-by-schema';",
173
+ "import dayjs from 'dayjs';",
174
+ ]
175
+
176
+ # Shared definitions
177
+ shared_defs = []
178
+
179
+ # Combine Type content
180
+ # Enums first
181
+ enums_content = list(global_ts_gen.enums.values())
182
+
183
+ types_content = ImportsAndContent(
184
+ imports, shared_defs + enums_content + all_interfaces
185
+ )
186
+
187
+ # Write Files
188
+ self.write_file(output_dir, "types.ts", types_content)
189
+
190
+ # Generate OptionsGetters
191
+ # Imports from types
192
+ enum_names = list(global_ts_gen.enums.keys())
193
+ getter_imports = []
194
+ if enum_names:
195
+ getter_imports.append(
196
+ f"import {{ {', '.join(sorted(enum_names))} }} from './types';"
197
+ )
198
+
199
+ getter_funcs = []
200
+ for enum_name, labels_map in global_ts_gen.enum_labels.items():
201
+ func_name = f"get{enum_name}Label"
202
+ lines = [
203
+ f"export function {func_name}(value: {enum_name} | null | undefined): string {{"
204
+ ]
205
+ lines.append(" if (value === null || value === undefined) return '';")
206
+ lines.append(" switch (value) {")
207
+
208
+ for val, label in labels_map.items():
209
+ # Value must match the enum definition in types.ts
210
+ # We used the same logic for keys in typescript_generator, but here we just need raw value for case
211
+ if isinstance(val, (int, float)):
212
+ case_val = f"{val}"
213
+ else:
214
+ # Escape quotes if string
215
+ safe_val = str(val).replace("'", "\\'")
216
+ case_val = f"'{safe_val}'"
217
+
218
+ # Escape label
219
+ safe_label = str(label).replace("'", "\\'").replace("\n", " ")
220
+ lines.append(f" case {case_val}: return '{safe_label}';")
221
+
222
+ lines.append(" default: return '';")
223
+ lines.append(" }")
224
+ lines.append("}")
225
+ getter_funcs.append("\n".join(lines))
226
+
227
+ # Export to index
228
+ # NO: these functions breach the SchemaType which are in the json files.
229
+ # index_exports.append(f"export * from './optionsGetters';")
230
+
231
+ getter_content_list = ImportsAndContent(getter_imports, getter_funcs)
232
+ self.write_file(output_dir, "optionsGetters.ts", getter_content_list)
233
+
234
+ # Write Index
235
+ # Dedup index exports
236
+ index_exports = list(dict.fromkeys(index_exports))
237
+ self.write_file(output_dir, "index.ts", index_exports)
238
+
239
+ self.stdout.write(
240
+ self.style.SUCCESS("Successfully synchronized schemas and types!")
241
+ )
242
+
243
+ def write_file(self, path, filename, content_list, custom_header=""):
244
+ full_path = os.path.join(path, filename)
245
+ if isinstance(content_list, ImportsAndContent):
246
+ content_str = content_list.to_string()
247
+ else:
248
+ content_str = "\n\n".join(content_list)
249
+
250
+ with open(full_path, "w") as f:
251
+ f.write("/* eslint-disable */\n")
252
+ f.write("// @generated by drf-react-by-schema\n\n")
253
+ if custom_header:
254
+ f.write(custom_header + "\n\n")
255
+ f.write(content_str)
256
+ f.write("\n")
257
+
258
+ def to_ts_literal(self, data, level=0):
259
+ # Kept for backward compatibility if other parts need it,
260
+ # but mostly unused for schema generation now
261
+ indent = " " * level
262
+
263
+ if isinstance(data, dict):
264
+ if not data:
265
+ return "{}"
266
+ lines = []
267
+ for key, value in data.items():
268
+ key = force_str(key)
269
+ if not key.isidentifier():
270
+ key = f"'{key}'"
271
+ val_str = self.to_ts_literal(value, level + 1)
272
+ lines.append(f"{indent} {key}: {val_str},")
273
+ return f"{{\n" + "\n".join(lines) + f"\n{indent}}}"
274
+
275
+ elif isinstance(data, (list, tuple)):
276
+ if not data:
277
+ return "[]"
278
+ # Multiline if any item is a dict or list or tuple
279
+ is_complex = any(isinstance(x, (dict, list, tuple)) for x in data)
280
+ items = [self.to_ts_literal(x, level + 1) for x in data]
281
+ if is_complex:
282
+ content = "\n".join([f"{indent} {x}," for x in items])
283
+ return f"[\n{content}\n{indent}]"
284
+ return f"[{', '.join(items)}]"
285
+
286
+ elif isinstance(data, bool):
287
+ return "true" if data else "false"
288
+
289
+ elif data is None:
290
+ return "null"
291
+
292
+ elif isinstance(data, (int, float)):
293
+ return str(data)
294
+
295
+ # Fallback for strings and anything else (including lazy objects)
296
+ val_str = force_str(data)
297
+
298
+ # If string contains single quote but NO double quote -> Use Double Quotes
299
+ # This makes strings like "('a', 'b')" cleaner: "('a', 'b')" instead of '(\'a\', \'b\')'
300
+ if "'" in val_str and '"' not in val_str:
301
+ safe_str = val_str.replace("\\", "\\\\").replace("\n", "\\n")
302
+ return f'"{safe_str}"'
303
+
304
+ # Default: Use Single Quotes
305
+ # Note: We must escape backslashes first, otherwise we might double-escape the ones added for quotes/newlines
306
+ safe_str = (
307
+ val_str.replace("\\", "\\\\").replace("\n", "\\n").replace("'", "\\'")
308
+ )
309
+ return f"'{safe_str}'"
310
+
311
+
312
+ class ImportsAndContent:
313
+ def __init__(self, imports, content):
314
+ self.imports = imports
315
+ self.content = content
316
+
317
+ def to_string(self):
318
+ return "\n".join(self.imports) + "\n\n" + "\n\n".join(self.content)
@@ -112,10 +112,15 @@ class Metadata(SimpleMetadata):
112
112
  elif getattr(field, "auto_now_add", False) or getattr(
113
113
  field, "auto_now", None
114
114
  ):
115
+ # metadata["actions"]["POST"][field_name]["model_default"] = (
116
+ # date.today()
117
+ # if isinstance(field, models.DateField)
118
+ # else datetime.now()
119
+ # )
115
120
  metadata["actions"]["POST"][field_name]["model_default"] = (
116
- date.today()
121
+ "{dateNow}"
117
122
  if isinstance(field, models.DateField)
118
- else datetime.now()
123
+ else "{dateTimeNow}"
119
124
  )
120
125
  elif serializer_field and serializer_field.default:
121
126
  if isinstance(serializer_field.default, (int, str)):
@@ -125,7 +130,7 @@ class Metadata(SimpleMetadata):
125
130
  elif isinstance(serializer_field.default, CurrentUserDefault):
126
131
  metadata["actions"]["POST"][field_name][
127
132
  "model_default"
128
- ] = "currentUser"
133
+ ] = "{currentUser}"
129
134
 
130
135
  # Custom regex Validators:
131
136
  validators = getattr(field, "validators", [])
@@ -80,11 +80,13 @@ class GenericSerializerMetaclass(SerializerMetaclass):
80
80
  # Help text and label
81
81
  help_text = getattr(field, "help_text", "")
82
82
  verbose_name = getattr(field, "verbose_name", "")
83
- if getattr(field.related_model._meta, "verbose_name", None):
83
+ if not verbose_name and getattr(
84
+ field.related_model._meta, "verbose_name", None
85
+ ):
84
86
  verbose_name = getattr(
85
87
  field.related_model._meta, "verbose_name", None
86
88
  )
87
- elif verbose_name == "":
89
+ elif not verbose_name:
88
90
  verbose_name = field.related_model.__name__
89
91
 
90
92
  # Reverse relations don't use null/blank the same way
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: drf-react-by-schema
3
- Version: 0.26.0
3
+ Version: 0.26.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
@@ -35,6 +35,9 @@ drf_react_by_schema/fields/number.py
35
35
  drf_react_by_schema/fields/relational.py
36
36
  drf_react_by_schema/fields/serializers.py
37
37
  drf_react_by_schema/fields/text.py
38
+ drf_react_by_schema/management/__init__.py
39
+ drf_react_by_schema/management/commands/__init__.py
40
+ drf_react_by_schema/management/commands/sync_react.py
38
41
  drf_react_by_schema/migrations/__init__.py
39
42
  drf_react_by_schema/patches/__init__.py
40
43
  drf_react_by_schema/patches/drf_serializers.py
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = drf_react_by_schema
3
- version = 0.26.0
3
+ version = 0.26.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