drf-to-mkdoc 0.1.0__py3-none-any.whl → 0.1.2__py3-none-any.whl
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.
Potentially problematic release.
This version of drf-to-mkdoc might be problematic. Click here for more details.
- drf_to_mkdoc/__init__.py +6 -6
- drf_to_mkdoc/apps.py +14 -14
- drf_to_mkdoc/conf/settings.py +44 -44
- drf_to_mkdoc/management/commands/build_docs.py +76 -76
- drf_to_mkdoc/management/commands/generate_doc_json.py +512 -512
- drf_to_mkdoc/management/commands/generate_docs.py +138 -138
- drf_to_mkdoc/management/commands/generate_model_docs.py +327 -327
- drf_to_mkdoc/management/commands/update_doc_schema.py +53 -53
- drf_to_mkdoc/utils/__init__.py +3 -3
- drf_to_mkdoc/utils/endpoint_generator.py +945 -945
- drf_to_mkdoc/utils/extractors/__init__.py +3 -3
- drf_to_mkdoc/utils/extractors/query_parameter_extractors.py +229 -229
- drf_to_mkdoc/utils/md_generators/query_parameters_generators.py +72 -72
- drf_to_mkdoc/utils/model_generator.py +269 -269
- {drf_to_mkdoc-0.1.0.dist-info → drf_to_mkdoc-0.1.2.dist-info}/METADATA +247 -247
- drf_to_mkdoc-0.1.2.dist-info/RECORD +25 -0
- {drf_to_mkdoc-0.1.0.dist-info → drf_to_mkdoc-0.1.2.dist-info}/licenses/LICENSE +21 -21
- drf_to_mkdoc-0.1.0.dist-info/RECORD +0 -25
- {drf_to_mkdoc-0.1.0.dist-info → drf_to_mkdoc-0.1.2.dist-info}/WHEEL +0 -0
- {drf_to_mkdoc-0.1.0.dist-info → drf_to_mkdoc-0.1.2.dist-info}/top_level.txt +0 -0
|
@@ -1,327 +1,327 @@
|
|
|
1
|
-
import inspect
|
|
2
|
-
import json
|
|
3
|
-
import re
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
|
|
6
|
-
from django.apps import apps
|
|
7
|
-
from django.core.management.base import BaseCommand
|
|
8
|
-
from django.db import models
|
|
9
|
-
|
|
10
|
-
from drf_to_mkdoc.conf.settings import drf_to_mkdoc_settings
|
|
11
|
-
|
|
12
|
-
class Command(BaseCommand):
|
|
13
|
-
help = "Generate model documentation JSON from Django model introspection"
|
|
14
|
-
|
|
15
|
-
def add_arguments(self, parser):
|
|
16
|
-
parser.add_argument(
|
|
17
|
-
"--output",
|
|
18
|
-
type=str,
|
|
19
|
-
default=drf_to_mkdoc_settings.MODEL_DOCS_FILE,
|
|
20
|
-
help=f"Output JSON file name (default: {drf_to_mkdoc_settings.MODEL_DOCS_FILE})",
|
|
21
|
-
)
|
|
22
|
-
parser.add_argument(
|
|
23
|
-
"--exclude-apps",
|
|
24
|
-
type=str,
|
|
25
|
-
nargs="*",
|
|
26
|
-
default=["admin", "auth", "contenttypes", "sessions", "messages", "staticfiles"],
|
|
27
|
-
help="Apps to exclude from documentation",
|
|
28
|
-
)
|
|
29
|
-
parser.add_argument("--pretty", action="store_true", help="Pretty print JSON output")
|
|
30
|
-
|
|
31
|
-
def handle(self, *args, **options):
|
|
32
|
-
output_file = options["output"]
|
|
33
|
-
exclude_apps = options["exclude_apps"]
|
|
34
|
-
pretty = options["pretty"]
|
|
35
|
-
|
|
36
|
-
self.stdout.write(self.style.SUCCESS("🔍 Scanning Django models..."))
|
|
37
|
-
|
|
38
|
-
model_docs = self.generate_model_documentation(exclude_apps)
|
|
39
|
-
|
|
40
|
-
json_data = {
|
|
41
|
-
"models": model_docs,
|
|
42
|
-
"stats": {
|
|
43
|
-
"total_models": len(model_docs),
|
|
44
|
-
"total_apps": len({model["app_label"] for model in model_docs.values()}),
|
|
45
|
-
},
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
output_path = Path(output_file)
|
|
49
|
-
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
50
|
-
with output_path.open("w", encoding="utf-8") as f:
|
|
51
|
-
if pretty:
|
|
52
|
-
json.dump(json_data, f, indent=2, ensure_ascii=False, default=str)
|
|
53
|
-
else:
|
|
54
|
-
json.dump(json_data, f, ensure_ascii=False, default=str)
|
|
55
|
-
|
|
56
|
-
self.stdout.write(
|
|
57
|
-
self.style.SUCCESS(f"✅ Generated model documentation: {output_path.absolute()}")
|
|
58
|
-
)
|
|
59
|
-
self.stdout.write(f"📊 Total models: {len(model_docs)}")
|
|
60
|
-
self.stdout.write(
|
|
61
|
-
f"📦 Total apps: {len({model['app_label'] for model in model_docs.values()})}"
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
def generate_model_documentation(self, exclude_apps):
|
|
65
|
-
"""Generate documentation for all Django models"""
|
|
66
|
-
model_docs = {}
|
|
67
|
-
|
|
68
|
-
for app_config in apps.get_app_configs():
|
|
69
|
-
app_label = app_config.label
|
|
70
|
-
|
|
71
|
-
# Skip excluded apps
|
|
72
|
-
if app_label in exclude_apps:
|
|
73
|
-
self.stdout.write(f"⏭️ Skipping app: {app_label}")
|
|
74
|
-
continue
|
|
75
|
-
|
|
76
|
-
self.stdout.write(f"📱 Processing app: {app_label}")
|
|
77
|
-
|
|
78
|
-
for model in app_config.get_models():
|
|
79
|
-
model_name = model.__name__
|
|
80
|
-
model_key = f"{app_label}.{model_name}"
|
|
81
|
-
|
|
82
|
-
self.stdout.write(f" 📋 Processing model: {model_name}")
|
|
83
|
-
|
|
84
|
-
model_docs[model_key] = self.introspect_model(model, app_label)
|
|
85
|
-
|
|
86
|
-
return model_docs
|
|
87
|
-
|
|
88
|
-
def introspect_model(self, model, app_label):
|
|
89
|
-
"""Introspect a single Django model"""
|
|
90
|
-
meta = model._meta
|
|
91
|
-
|
|
92
|
-
# Basic model information
|
|
93
|
-
model_doc = {
|
|
94
|
-
"name": model.__name__,
|
|
95
|
-
"app_label": app_label,
|
|
96
|
-
"table_name": meta.db_table,
|
|
97
|
-
"verbose_name": str(meta.verbose_name),
|
|
98
|
-
"verbose_name_plural": str(meta.verbose_name_plural),
|
|
99
|
-
"description": self.get_model_description(model),
|
|
100
|
-
"abstract": meta.abstract,
|
|
101
|
-
"proxy": meta.proxy,
|
|
102
|
-
"fields": {},
|
|
103
|
-
"relationships": {},
|
|
104
|
-
"meta_options": self.get_meta_options(meta),
|
|
105
|
-
"methods": self.get_model_methods(model),
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
# Process fields
|
|
109
|
-
for field in meta.get_fields():
|
|
110
|
-
if field.many_to_many or field.one_to_many or field.many_to_one or field.one_to_one:
|
|
111
|
-
# Handle relationships separately
|
|
112
|
-
model_doc["relationships"][field.name] = self.introspect_relationship(field)
|
|
113
|
-
else:
|
|
114
|
-
# Handle regular fields
|
|
115
|
-
model_doc["fields"][field.name] = self.introspect_field(field)
|
|
116
|
-
|
|
117
|
-
return model_doc
|
|
118
|
-
|
|
119
|
-
def introspect_field(self, field):
|
|
120
|
-
"""Introspect a single model field"""
|
|
121
|
-
return {
|
|
122
|
-
"name": field.name,
|
|
123
|
-
"type": field.__class__.__name__,
|
|
124
|
-
"verbose_name": (
|
|
125
|
-
str(field.verbose_name) if hasattr(field, "verbose_name") else field.name
|
|
126
|
-
),
|
|
127
|
-
"help_text": field.help_text if hasattr(field, "help_text") else "",
|
|
128
|
-
"null": getattr(field, "null", False),
|
|
129
|
-
"blank": getattr(field, "blank", False),
|
|
130
|
-
"editable": getattr(field, "editable", True),
|
|
131
|
-
"primary_key": getattr(field, "primary_key", False),
|
|
132
|
-
"unique": getattr(field, "unique", False),
|
|
133
|
-
"db_index": getattr(field, "db_index", False),
|
|
134
|
-
"default": self.get_field_default(field),
|
|
135
|
-
"choices": self.get_field_choices(field),
|
|
136
|
-
"validators": self.get_field_validators(field),
|
|
137
|
-
"field_specific": self.get_field_specific_attrs(field),
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
def introspect_relationship(self, field):
|
|
141
|
-
"""Introspect relationship fields"""
|
|
142
|
-
return {
|
|
143
|
-
"name": field.name,
|
|
144
|
-
"type": field.__class__.__name__,
|
|
145
|
-
"related_model": f"{field.related_model._meta.app_label}."
|
|
146
|
-
f"{field.related_model.__name__}",
|
|
147
|
-
"related_name": getattr(field, "related_name", None),
|
|
148
|
-
"on_delete": self.get_on_delete_name(field),
|
|
149
|
-
"null": getattr(field, "null", False),
|
|
150
|
-
"blank": getattr(field, "blank", False),
|
|
151
|
-
"many_to_many": field.many_to_many,
|
|
152
|
-
"one_to_many": field.one_to_many,
|
|
153
|
-
"many_to_one": field.many_to_one,
|
|
154
|
-
"one_to_one": field.one_to_one,
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
def get_on_delete_name(self, field):
|
|
158
|
-
"""Get readable name for on_delete option"""
|
|
159
|
-
if not hasattr(field, "on_delete") or field.on_delete is None:
|
|
160
|
-
return None
|
|
161
|
-
|
|
162
|
-
# Import Django's on_delete functions
|
|
163
|
-
|
|
164
|
-
# Map function objects to their readable names
|
|
165
|
-
on_delete_mapping = {
|
|
166
|
-
models.CASCADE: "CASCADE",
|
|
167
|
-
models.PROTECT: "PROTECT",
|
|
168
|
-
models.SET_NULL: "SET_NULL",
|
|
169
|
-
models.SET_DEFAULT: "SET_DEFAULT",
|
|
170
|
-
models.SET: "SET",
|
|
171
|
-
models.DO_NOTHING: "DO_NOTHING",
|
|
172
|
-
models.RESTRICT: "RESTRICT",
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
on_delete_func = field.on_delete
|
|
176
|
-
|
|
177
|
-
# Handle SET() callable
|
|
178
|
-
if hasattr(on_delete_func, "__name__") and on_delete_func.__name__ == "SET":
|
|
179
|
-
return "SET"
|
|
180
|
-
|
|
181
|
-
# Check if it's one of the standard Django on_delete options
|
|
182
|
-
for func, name in on_delete_mapping.items():
|
|
183
|
-
if on_delete_func is func:
|
|
184
|
-
return name
|
|
185
|
-
|
|
186
|
-
# Fallback for custom functions or unknown cases
|
|
187
|
-
return getattr(on_delete_func, "__name__", str(on_delete_func))
|
|
188
|
-
|
|
189
|
-
def get_model_description(self, model):
|
|
190
|
-
"""Get model description from docstring or generate one"""
|
|
191
|
-
if model.__doc__ and not model.__doc__.startswith(model.__name__ + "("):
|
|
192
|
-
# Only use docstring if it's not just the auto-generated field list
|
|
193
|
-
return model.__doc__.strip()
|
|
194
|
-
return ""
|
|
195
|
-
|
|
196
|
-
def get_meta_options(self, meta):
|
|
197
|
-
"""Extract Meta class options"""
|
|
198
|
-
options = {}
|
|
199
|
-
|
|
200
|
-
# Common Meta options
|
|
201
|
-
meta_attrs = [
|
|
202
|
-
"ordering",
|
|
203
|
-
"unique_together",
|
|
204
|
-
"index_together",
|
|
205
|
-
"constraints",
|
|
206
|
-
"indexes",
|
|
207
|
-
"permissions",
|
|
208
|
-
"default_permissions",
|
|
209
|
-
"get_latest_by",
|
|
210
|
-
"order_with_respect_to",
|
|
211
|
-
"managed",
|
|
212
|
-
"default_manager_name",
|
|
213
|
-
]
|
|
214
|
-
|
|
215
|
-
for attr in meta_attrs:
|
|
216
|
-
if hasattr(meta, attr):
|
|
217
|
-
value = getattr(meta, attr)
|
|
218
|
-
if value:
|
|
219
|
-
options[attr] = str(value)
|
|
220
|
-
|
|
221
|
-
return options
|
|
222
|
-
|
|
223
|
-
def get_model_methods(self, model):
|
|
224
|
-
"""Get custom model methods (excluding built-in Django methods)"""
|
|
225
|
-
methods = []
|
|
226
|
-
|
|
227
|
-
# Get all methods that don't start with underscore and aren't Django built-ins
|
|
228
|
-
django_methods = {
|
|
229
|
-
"save",
|
|
230
|
-
"delete",
|
|
231
|
-
"clean",
|
|
232
|
-
"full_clean",
|
|
233
|
-
"validate_unique",
|
|
234
|
-
"get_absolute_url",
|
|
235
|
-
"get_next_by_",
|
|
236
|
-
"get_previous_by_",
|
|
237
|
-
"refresh_from_db",
|
|
238
|
-
"serializable_value",
|
|
239
|
-
"check",
|
|
240
|
-
"from_db",
|
|
241
|
-
"clean_fields",
|
|
242
|
-
"get_deferred_fields",
|
|
243
|
-
"pk",
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
model_field_names = {field.name for field in model._meta.get_fields()}
|
|
247
|
-
|
|
248
|
-
for attr_name in dir(model):
|
|
249
|
-
if (
|
|
250
|
-
not attr_name.startswith("_")
|
|
251
|
-
and not attr_name.startswith("get_next_by_")
|
|
252
|
-
and not attr_name.startswith("get_previous_by_")
|
|
253
|
-
and attr_name not in django_methods
|
|
254
|
-
and callable(getattr(model, attr_name))
|
|
255
|
-
):
|
|
256
|
-
display_method_match = re.match(r"^get_(.+)_display$", attr_name)
|
|
257
|
-
if display_method_match:
|
|
258
|
-
field_name = display_method_match.group(1)
|
|
259
|
-
if field_name in model_field_names:
|
|
260
|
-
# Exclude if the field doesn't exist in the model
|
|
261
|
-
continue
|
|
262
|
-
|
|
263
|
-
method = getattr(model, attr_name)
|
|
264
|
-
|
|
265
|
-
# Check if it's a method defined in this class (not inherited from Django)
|
|
266
|
-
if (
|
|
267
|
-
inspect.ismethod(method)
|
|
268
|
-
or inspect.isfunction(method)
|
|
269
|
-
or (hasattr(method, "__func__") and inspect.isfunction(method.__func__))
|
|
270
|
-
):
|
|
271
|
-
# Check if method is defined in the model class itself
|
|
272
|
-
if hasattr(model, attr_name) and attr_name in model.__dict__:
|
|
273
|
-
methods.append(
|
|
274
|
-
{
|
|
275
|
-
"name": attr_name,
|
|
276
|
-
"docstring": method.__doc__.strip() if method.__doc__ else "",
|
|
277
|
-
}
|
|
278
|
-
)
|
|
279
|
-
|
|
280
|
-
return methods
|
|
281
|
-
|
|
282
|
-
def get_field_default(self, field):
|
|
283
|
-
"""Get field default value"""
|
|
284
|
-
if hasattr(field, "default") and field.default is not models.NOT_PROVIDED:
|
|
285
|
-
default = field.default
|
|
286
|
-
if callable(default):
|
|
287
|
-
return f"<callable: {default.__name__}>"
|
|
288
|
-
return str(default)
|
|
289
|
-
return None
|
|
290
|
-
|
|
291
|
-
def get_field_choices(self, field):
|
|
292
|
-
"""Get field choices"""
|
|
293
|
-
if hasattr(field, "choices") and field.choices:
|
|
294
|
-
return [{"value": choice[0], "display": choice[1]} for choice in field.choices]
|
|
295
|
-
return []
|
|
296
|
-
|
|
297
|
-
def get_field_validators(self, field):
|
|
298
|
-
"""Get field validators"""
|
|
299
|
-
if hasattr(field, "validators") and field.validators:
|
|
300
|
-
return [validator.__class__.__name__ for validator in field.validators]
|
|
301
|
-
return []
|
|
302
|
-
|
|
303
|
-
def get_field_specific_attrs(self, field):
|
|
304
|
-
"""Get field-specific attributes"""
|
|
305
|
-
specific_attrs = {}
|
|
306
|
-
|
|
307
|
-
# CharField, TextField
|
|
308
|
-
if hasattr(field, "max_length") and field.max_length:
|
|
309
|
-
specific_attrs["max_length"] = field.max_length
|
|
310
|
-
|
|
311
|
-
# DecimalField
|
|
312
|
-
if hasattr(field, "max_digits") and field.max_digits:
|
|
313
|
-
specific_attrs["max_digits"] = field.max_digits
|
|
314
|
-
if hasattr(field, "decimal_places") and field.decimal_places:
|
|
315
|
-
specific_attrs["decimal_places"] = field.decimal_places
|
|
316
|
-
|
|
317
|
-
# FileField, ImageField
|
|
318
|
-
if hasattr(field, "upload_to") and field.upload_to:
|
|
319
|
-
specific_attrs["upload_to"] = str(field.upload_to)
|
|
320
|
-
|
|
321
|
-
# DateTimeField
|
|
322
|
-
if hasattr(field, "auto_now") and field.auto_now:
|
|
323
|
-
specific_attrs["auto_now"] = field.auto_now
|
|
324
|
-
if hasattr(field, "auto_now_add") and field.auto_now_add:
|
|
325
|
-
specific_attrs["auto_now_add"] = field.auto_now_add
|
|
326
|
-
|
|
327
|
-
return specific_attrs
|
|
1
|
+
import inspect
|
|
2
|
+
import json
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from django.apps import apps
|
|
7
|
+
from django.core.management.base import BaseCommand
|
|
8
|
+
from django.db import models
|
|
9
|
+
|
|
10
|
+
from drf_to_mkdoc.conf.settings import drf_to_mkdoc_settings
|
|
11
|
+
|
|
12
|
+
class Command(BaseCommand):
|
|
13
|
+
help = "Generate model documentation JSON from Django model introspection"
|
|
14
|
+
|
|
15
|
+
def add_arguments(self, parser):
|
|
16
|
+
parser.add_argument(
|
|
17
|
+
"--output",
|
|
18
|
+
type=str,
|
|
19
|
+
default=drf_to_mkdoc_settings.MODEL_DOCS_FILE,
|
|
20
|
+
help=f"Output JSON file name (default: {drf_to_mkdoc_settings.MODEL_DOCS_FILE})",
|
|
21
|
+
)
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"--exclude-apps",
|
|
24
|
+
type=str,
|
|
25
|
+
nargs="*",
|
|
26
|
+
default=["admin", "auth", "contenttypes", "sessions", "messages", "staticfiles"],
|
|
27
|
+
help="Apps to exclude from documentation",
|
|
28
|
+
)
|
|
29
|
+
parser.add_argument("--pretty", action="store_true", help="Pretty print JSON output")
|
|
30
|
+
|
|
31
|
+
def handle(self, *args, **options):
|
|
32
|
+
output_file = options["output"]
|
|
33
|
+
exclude_apps = options["exclude_apps"]
|
|
34
|
+
pretty = options["pretty"]
|
|
35
|
+
|
|
36
|
+
self.stdout.write(self.style.SUCCESS("🔍 Scanning Django models..."))
|
|
37
|
+
|
|
38
|
+
model_docs = self.generate_model_documentation(exclude_apps)
|
|
39
|
+
|
|
40
|
+
json_data = {
|
|
41
|
+
"models": model_docs,
|
|
42
|
+
"stats": {
|
|
43
|
+
"total_models": len(model_docs),
|
|
44
|
+
"total_apps": len({model["app_label"] for model in model_docs.values()}),
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
output_path = Path(output_file)
|
|
49
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
50
|
+
with output_path.open("w", encoding="utf-8") as f:
|
|
51
|
+
if pretty:
|
|
52
|
+
json.dump(json_data, f, indent=2, ensure_ascii=False, default=str)
|
|
53
|
+
else:
|
|
54
|
+
json.dump(json_data, f, ensure_ascii=False, default=str)
|
|
55
|
+
|
|
56
|
+
self.stdout.write(
|
|
57
|
+
self.style.SUCCESS(f"✅ Generated model documentation: {output_path.absolute()}")
|
|
58
|
+
)
|
|
59
|
+
self.stdout.write(f"📊 Total models: {len(model_docs)}")
|
|
60
|
+
self.stdout.write(
|
|
61
|
+
f"📦 Total apps: {len({model['app_label'] for model in model_docs.values()})}"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def generate_model_documentation(self, exclude_apps):
|
|
65
|
+
"""Generate documentation for all Django models"""
|
|
66
|
+
model_docs = {}
|
|
67
|
+
|
|
68
|
+
for app_config in apps.get_app_configs():
|
|
69
|
+
app_label = app_config.label
|
|
70
|
+
|
|
71
|
+
# Skip excluded apps
|
|
72
|
+
if app_label in exclude_apps:
|
|
73
|
+
self.stdout.write(f"⏭️ Skipping app: {app_label}")
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
self.stdout.write(f"📱 Processing app: {app_label}")
|
|
77
|
+
|
|
78
|
+
for model in app_config.get_models():
|
|
79
|
+
model_name = model.__name__
|
|
80
|
+
model_key = f"{app_label}.{model_name}"
|
|
81
|
+
|
|
82
|
+
self.stdout.write(f" 📋 Processing model: {model_name}")
|
|
83
|
+
|
|
84
|
+
model_docs[model_key] = self.introspect_model(model, app_label)
|
|
85
|
+
|
|
86
|
+
return model_docs
|
|
87
|
+
|
|
88
|
+
def introspect_model(self, model, app_label):
|
|
89
|
+
"""Introspect a single Django model"""
|
|
90
|
+
meta = model._meta
|
|
91
|
+
|
|
92
|
+
# Basic model information
|
|
93
|
+
model_doc = {
|
|
94
|
+
"name": model.__name__,
|
|
95
|
+
"app_label": app_label,
|
|
96
|
+
"table_name": meta.db_table,
|
|
97
|
+
"verbose_name": str(meta.verbose_name),
|
|
98
|
+
"verbose_name_plural": str(meta.verbose_name_plural),
|
|
99
|
+
"description": self.get_model_description(model),
|
|
100
|
+
"abstract": meta.abstract,
|
|
101
|
+
"proxy": meta.proxy,
|
|
102
|
+
"fields": {},
|
|
103
|
+
"relationships": {},
|
|
104
|
+
"meta_options": self.get_meta_options(meta),
|
|
105
|
+
"methods": self.get_model_methods(model),
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# Process fields
|
|
109
|
+
for field in meta.get_fields():
|
|
110
|
+
if field.many_to_many or field.one_to_many or field.many_to_one or field.one_to_one:
|
|
111
|
+
# Handle relationships separately
|
|
112
|
+
model_doc["relationships"][field.name] = self.introspect_relationship(field)
|
|
113
|
+
else:
|
|
114
|
+
# Handle regular fields
|
|
115
|
+
model_doc["fields"][field.name] = self.introspect_field(field)
|
|
116
|
+
|
|
117
|
+
return model_doc
|
|
118
|
+
|
|
119
|
+
def introspect_field(self, field):
|
|
120
|
+
"""Introspect a single model field"""
|
|
121
|
+
return {
|
|
122
|
+
"name": field.name,
|
|
123
|
+
"type": field.__class__.__name__,
|
|
124
|
+
"verbose_name": (
|
|
125
|
+
str(field.verbose_name) if hasattr(field, "verbose_name") else field.name
|
|
126
|
+
),
|
|
127
|
+
"help_text": field.help_text if hasattr(field, "help_text") else "",
|
|
128
|
+
"null": getattr(field, "null", False),
|
|
129
|
+
"blank": getattr(field, "blank", False),
|
|
130
|
+
"editable": getattr(field, "editable", True),
|
|
131
|
+
"primary_key": getattr(field, "primary_key", False),
|
|
132
|
+
"unique": getattr(field, "unique", False),
|
|
133
|
+
"db_index": getattr(field, "db_index", False),
|
|
134
|
+
"default": self.get_field_default(field),
|
|
135
|
+
"choices": self.get_field_choices(field),
|
|
136
|
+
"validators": self.get_field_validators(field),
|
|
137
|
+
"field_specific": self.get_field_specific_attrs(field),
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
def introspect_relationship(self, field):
|
|
141
|
+
"""Introspect relationship fields"""
|
|
142
|
+
return {
|
|
143
|
+
"name": field.name,
|
|
144
|
+
"type": field.__class__.__name__,
|
|
145
|
+
"related_model": f"{field.related_model._meta.app_label}."
|
|
146
|
+
f"{field.related_model.__name__}",
|
|
147
|
+
"related_name": getattr(field, "related_name", None),
|
|
148
|
+
"on_delete": self.get_on_delete_name(field),
|
|
149
|
+
"null": getattr(field, "null", False),
|
|
150
|
+
"blank": getattr(field, "blank", False),
|
|
151
|
+
"many_to_many": field.many_to_many,
|
|
152
|
+
"one_to_many": field.one_to_many,
|
|
153
|
+
"many_to_one": field.many_to_one,
|
|
154
|
+
"one_to_one": field.one_to_one,
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
def get_on_delete_name(self, field):
|
|
158
|
+
"""Get readable name for on_delete option"""
|
|
159
|
+
if not hasattr(field, "on_delete") or field.on_delete is None:
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
# Import Django's on_delete functions
|
|
163
|
+
|
|
164
|
+
# Map function objects to their readable names
|
|
165
|
+
on_delete_mapping = {
|
|
166
|
+
models.CASCADE: "CASCADE",
|
|
167
|
+
models.PROTECT: "PROTECT",
|
|
168
|
+
models.SET_NULL: "SET_NULL",
|
|
169
|
+
models.SET_DEFAULT: "SET_DEFAULT",
|
|
170
|
+
models.SET: "SET",
|
|
171
|
+
models.DO_NOTHING: "DO_NOTHING",
|
|
172
|
+
models.RESTRICT: "RESTRICT",
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
on_delete_func = field.on_delete
|
|
176
|
+
|
|
177
|
+
# Handle SET() callable
|
|
178
|
+
if hasattr(on_delete_func, "__name__") and on_delete_func.__name__ == "SET":
|
|
179
|
+
return "SET"
|
|
180
|
+
|
|
181
|
+
# Check if it's one of the standard Django on_delete options
|
|
182
|
+
for func, name in on_delete_mapping.items():
|
|
183
|
+
if on_delete_func is func:
|
|
184
|
+
return name
|
|
185
|
+
|
|
186
|
+
# Fallback for custom functions or unknown cases
|
|
187
|
+
return getattr(on_delete_func, "__name__", str(on_delete_func))
|
|
188
|
+
|
|
189
|
+
def get_model_description(self, model):
|
|
190
|
+
"""Get model description from docstring or generate one"""
|
|
191
|
+
if model.__doc__ and not model.__doc__.startswith(model.__name__ + "("):
|
|
192
|
+
# Only use docstring if it's not just the auto-generated field list
|
|
193
|
+
return model.__doc__.strip()
|
|
194
|
+
return ""
|
|
195
|
+
|
|
196
|
+
def get_meta_options(self, meta):
|
|
197
|
+
"""Extract Meta class options"""
|
|
198
|
+
options = {}
|
|
199
|
+
|
|
200
|
+
# Common Meta options
|
|
201
|
+
meta_attrs = [
|
|
202
|
+
"ordering",
|
|
203
|
+
"unique_together",
|
|
204
|
+
"index_together",
|
|
205
|
+
"constraints",
|
|
206
|
+
"indexes",
|
|
207
|
+
"permissions",
|
|
208
|
+
"default_permissions",
|
|
209
|
+
"get_latest_by",
|
|
210
|
+
"order_with_respect_to",
|
|
211
|
+
"managed",
|
|
212
|
+
"default_manager_name",
|
|
213
|
+
]
|
|
214
|
+
|
|
215
|
+
for attr in meta_attrs:
|
|
216
|
+
if hasattr(meta, attr):
|
|
217
|
+
value = getattr(meta, attr)
|
|
218
|
+
if value:
|
|
219
|
+
options[attr] = str(value)
|
|
220
|
+
|
|
221
|
+
return options
|
|
222
|
+
|
|
223
|
+
def get_model_methods(self, model):
|
|
224
|
+
"""Get custom model methods (excluding built-in Django methods)"""
|
|
225
|
+
methods = []
|
|
226
|
+
|
|
227
|
+
# Get all methods that don't start with underscore and aren't Django built-ins
|
|
228
|
+
django_methods = {
|
|
229
|
+
"save",
|
|
230
|
+
"delete",
|
|
231
|
+
"clean",
|
|
232
|
+
"full_clean",
|
|
233
|
+
"validate_unique",
|
|
234
|
+
"get_absolute_url",
|
|
235
|
+
"get_next_by_",
|
|
236
|
+
"get_previous_by_",
|
|
237
|
+
"refresh_from_db",
|
|
238
|
+
"serializable_value",
|
|
239
|
+
"check",
|
|
240
|
+
"from_db",
|
|
241
|
+
"clean_fields",
|
|
242
|
+
"get_deferred_fields",
|
|
243
|
+
"pk",
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
model_field_names = {field.name for field in model._meta.get_fields()}
|
|
247
|
+
|
|
248
|
+
for attr_name in dir(model):
|
|
249
|
+
if (
|
|
250
|
+
not attr_name.startswith("_")
|
|
251
|
+
and not attr_name.startswith("get_next_by_")
|
|
252
|
+
and not attr_name.startswith("get_previous_by_")
|
|
253
|
+
and attr_name not in django_methods
|
|
254
|
+
and callable(getattr(model, attr_name))
|
|
255
|
+
):
|
|
256
|
+
display_method_match = re.match(r"^get_(.+)_display$", attr_name)
|
|
257
|
+
if display_method_match:
|
|
258
|
+
field_name = display_method_match.group(1)
|
|
259
|
+
if field_name in model_field_names:
|
|
260
|
+
# Exclude if the field doesn't exist in the model
|
|
261
|
+
continue
|
|
262
|
+
|
|
263
|
+
method = getattr(model, attr_name)
|
|
264
|
+
|
|
265
|
+
# Check if it's a method defined in this class (not inherited from Django)
|
|
266
|
+
if (
|
|
267
|
+
inspect.ismethod(method)
|
|
268
|
+
or inspect.isfunction(method)
|
|
269
|
+
or (hasattr(method, "__func__") and inspect.isfunction(method.__func__))
|
|
270
|
+
):
|
|
271
|
+
# Check if method is defined in the model class itself
|
|
272
|
+
if hasattr(model, attr_name) and attr_name in model.__dict__:
|
|
273
|
+
methods.append(
|
|
274
|
+
{
|
|
275
|
+
"name": attr_name,
|
|
276
|
+
"docstring": method.__doc__.strip() if method.__doc__ else "",
|
|
277
|
+
}
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
return methods
|
|
281
|
+
|
|
282
|
+
def get_field_default(self, field):
|
|
283
|
+
"""Get field default value"""
|
|
284
|
+
if hasattr(field, "default") and field.default is not models.NOT_PROVIDED:
|
|
285
|
+
default = field.default
|
|
286
|
+
if callable(default):
|
|
287
|
+
return f"<callable: {default.__name__}>"
|
|
288
|
+
return str(default)
|
|
289
|
+
return None
|
|
290
|
+
|
|
291
|
+
def get_field_choices(self, field):
|
|
292
|
+
"""Get field choices"""
|
|
293
|
+
if hasattr(field, "choices") and field.choices:
|
|
294
|
+
return [{"value": choice[0], "display": choice[1]} for choice in field.choices]
|
|
295
|
+
return []
|
|
296
|
+
|
|
297
|
+
def get_field_validators(self, field):
|
|
298
|
+
"""Get field validators"""
|
|
299
|
+
if hasattr(field, "validators") and field.validators:
|
|
300
|
+
return [validator.__class__.__name__ for validator in field.validators]
|
|
301
|
+
return []
|
|
302
|
+
|
|
303
|
+
def get_field_specific_attrs(self, field):
|
|
304
|
+
"""Get field-specific attributes"""
|
|
305
|
+
specific_attrs = {}
|
|
306
|
+
|
|
307
|
+
# CharField, TextField
|
|
308
|
+
if hasattr(field, "max_length") and field.max_length:
|
|
309
|
+
specific_attrs["max_length"] = field.max_length
|
|
310
|
+
|
|
311
|
+
# DecimalField
|
|
312
|
+
if hasattr(field, "max_digits") and field.max_digits:
|
|
313
|
+
specific_attrs["max_digits"] = field.max_digits
|
|
314
|
+
if hasattr(field, "decimal_places") and field.decimal_places:
|
|
315
|
+
specific_attrs["decimal_places"] = field.decimal_places
|
|
316
|
+
|
|
317
|
+
# FileField, ImageField
|
|
318
|
+
if hasattr(field, "upload_to") and field.upload_to:
|
|
319
|
+
specific_attrs["upload_to"] = str(field.upload_to)
|
|
320
|
+
|
|
321
|
+
# DateTimeField
|
|
322
|
+
if hasattr(field, "auto_now") and field.auto_now:
|
|
323
|
+
specific_attrs["auto_now"] = field.auto_now
|
|
324
|
+
if hasattr(field, "auto_now_add") and field.auto_now_add:
|
|
325
|
+
specific_attrs["auto_now_add"] = field.auto_now_add
|
|
326
|
+
|
|
327
|
+
return specific_attrs
|