drf-to-mkdoc 0.2.0__py3-none-any.whl → 0.2.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/conf/defaults.py +5 -0
- drf_to_mkdoc/conf/settings.py +121 -9
- drf_to_mkdoc/management/commands/build_docs.py +8 -7
- drf_to_mkdoc/management/commands/build_endpoint_docs.py +69 -0
- drf_to_mkdoc/management/commands/build_model_docs.py +50 -0
- drf_to_mkdoc/management/commands/{generate_model_docs.py → extract_model_data.py} +14 -19
- drf_to_mkdoc/templates/endpoints/detail/base.html +33 -0
- drf_to_mkdoc/templates/endpoints/detail/path_parameters.html +8 -0
- drf_to_mkdoc/templates/endpoints/detail/query_parameters.html +43 -0
- drf_to_mkdoc/templates/endpoints/detail/request_body.html +10 -0
- drf_to_mkdoc/templates/endpoints/detail/responses.html +18 -0
- drf_to_mkdoc/templates/endpoints/list/base.html +23 -0
- drf_to_mkdoc/templates/endpoints/list/endpoint_card.html +18 -0
- drf_to_mkdoc/templates/endpoints/list/filter_section.html +16 -0
- drf_to_mkdoc/templates/endpoints/list/filters/app.html +8 -0
- drf_to_mkdoc/templates/endpoints/list/filters/method.html +12 -0
- drf_to_mkdoc/templates/endpoints/list/filters/path.html +5 -0
- drf_to_mkdoc/templates/endpoints/list/filters/search.html +9 -0
- drf_to_mkdoc/templates/model_detail/base.html +34 -0
- drf_to_mkdoc/templates/model_detail/choices.html +12 -0
- drf_to_mkdoc/templates/model_detail/fields.html +11 -0
- drf_to_mkdoc/templates/model_detail/meta.html +6 -0
- drf_to_mkdoc/templates/model_detail/methods.html +9 -0
- drf_to_mkdoc/templates/model_detail/relationships.html +8 -0
- drf_to_mkdoc/templates/models_index.html +24 -0
- drf_to_mkdoc/templatetags/custom_filters.py +116 -0
- drf_to_mkdoc/utils/ai_tools/enums.py +13 -0
- drf_to_mkdoc/utils/ai_tools/exceptions.py +19 -0
- drf_to_mkdoc/utils/ai_tools/providers/__init__.py +0 -0
- drf_to_mkdoc/utils/ai_tools/providers/base_provider.py +123 -0
- drf_to_mkdoc/utils/ai_tools/providers/gemini_provider.py +80 -0
- drf_to_mkdoc/utils/ai_tools/types.py +81 -0
- drf_to_mkdoc/utils/commons/__init__.py +0 -0
- drf_to_mkdoc/utils/commons/code_extractor.py +22 -0
- drf_to_mkdoc/utils/commons/file_utils.py +35 -0
- drf_to_mkdoc/utils/commons/model_utils.py +83 -0
- drf_to_mkdoc/utils/commons/operation_utils.py +83 -0
- drf_to_mkdoc/utils/commons/path_utils.py +78 -0
- drf_to_mkdoc/utils/commons/schema_utils.py +230 -0
- drf_to_mkdoc/utils/endpoint_detail_generator.py +86 -202
- drf_to_mkdoc/utils/endpoint_list_generator.py +59 -194
- drf_to_mkdoc/utils/extractors/query_parameter_extractors.py +33 -30
- drf_to_mkdoc/utils/model_detail_generator.py +37 -211
- drf_to_mkdoc/utils/model_list_generator.py +38 -46
- drf_to_mkdoc/utils/schema.py +259 -0
- {drf_to_mkdoc-0.2.0.dist-info → drf_to_mkdoc-0.2.2.dist-info}/METADATA +16 -5
- drf_to_mkdoc-0.2.2.dist-info/RECORD +85 -0
- drf_to_mkdoc/management/commands/generate_docs.py +0 -113
- drf_to_mkdoc/utils/common.py +0 -353
- drf_to_mkdoc/utils/md_generators/query_parameters_generators.py +0 -72
- drf_to_mkdoc-0.2.0.dist-info/RECORD +0 -52
- /drf_to_mkdoc/utils/{md_generators → ai_tools}/__init__.py +0 -0
- {drf_to_mkdoc-0.2.0.dist-info → drf_to_mkdoc-0.2.2.dist-info}/WHEEL +0 -0
- {drf_to_mkdoc-0.2.0.dist-info → drf_to_mkdoc-0.2.2.dist-info}/licenses/LICENSE +0 -0
- {drf_to_mkdoc-0.2.0.dist-info → drf_to_mkdoc-0.2.2.dist-info}/top_level.txt +0 -0
drf_to_mkdoc/conf/defaults.py
CHANGED
|
@@ -7,6 +7,11 @@ DEFAULTS = {
|
|
|
7
7
|
"CUSTOM_SCHEMA_FILE": "docs/configs/custom_schema.json", # Path to custom schema file
|
|
8
8
|
"PATH_PARAM_SUBSTITUTE_FUNCTION": None,
|
|
9
9
|
"PATH_PARAM_SUBSTITUTE_MAPPING": {},
|
|
10
|
+
# AI documentation settings
|
|
11
|
+
"ENABLE_AI_DOCS": False,
|
|
12
|
+
"AI_CONFIG_DIR_NAME": "ai_code", # Directory name for AI-generated code files
|
|
13
|
+
"AI_OPERATION_MAP_FILE": "docs/configs/operation_map.json", # Path to operation map file
|
|
14
|
+
"SERIALIZERS_INHERITANCE_DEPTH": 1, # Maximum depth for class inheritance analysis
|
|
10
15
|
# Django apps - required, no default
|
|
11
16
|
"DJANGO_APPS": None, # List of Django app names to process
|
|
12
17
|
}
|
drf_to_mkdoc/conf/settings.py
CHANGED
|
@@ -1,31 +1,143 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Any, ClassVar
|
|
3
|
+
|
|
1
4
|
from django.conf import settings
|
|
2
5
|
|
|
3
6
|
from drf_to_mkdoc.conf.defaults import DEFAULTS
|
|
4
7
|
|
|
5
8
|
|
|
6
9
|
class DRFToMkDocSettings:
|
|
7
|
-
required_settings = ["DJANGO_APPS"]
|
|
8
|
-
project_settings = {"PROJECT_NAME": "drf-to-mkdoc"}
|
|
10
|
+
required_settings: ClassVar[list[str]] = ["DJANGO_APPS"]
|
|
11
|
+
project_settings: ClassVar[dict[str, Any]] = {"PROJECT_NAME": "drf-to-mkdoc"}
|
|
12
|
+
|
|
13
|
+
settings_types: ClassVar[dict[str, type]] = {
|
|
14
|
+
"ENABLE_AI_DOCS": bool,
|
|
15
|
+
"AI_CONFIG_DIR_NAME": str,
|
|
16
|
+
"SERIALIZERS_INHERITANCE_DEPTH": int,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
settings_ranges: ClassVar[dict[str, tuple[int, int]]] = {
|
|
20
|
+
"SERIALIZERS_INHERITANCE_DEPTH": (1, 3),
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
path_settings = {
|
|
24
|
+
"CONFIG_DIR",
|
|
25
|
+
"MODEL_DOCS_FILE",
|
|
26
|
+
"DOC_CONFIG_FILE",
|
|
27
|
+
"CUSTOM_SCHEMA_FILE",
|
|
28
|
+
"AI_OPERATION_MAP_FILE",
|
|
29
|
+
}
|
|
9
30
|
|
|
10
31
|
def __init__(self, user_settings_key="DRF_TO_MKDOC", defaults=None):
|
|
11
32
|
self.user_settings_key = user_settings_key
|
|
12
33
|
self._user_settings = getattr(settings, user_settings_key, {})
|
|
13
34
|
self.defaults = defaults or {}
|
|
14
35
|
|
|
15
|
-
def
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
36
|
+
def _validate_type(self, key: str, value: Any) -> None:
|
|
37
|
+
"""Validate the type of setting value."""
|
|
38
|
+
if key in self.settings_types:
|
|
39
|
+
expected_type = self.settings_types[key]
|
|
40
|
+
if not isinstance(value, expected_type):
|
|
41
|
+
raise TypeError(
|
|
42
|
+
f"DRF_TO_MKDOC setting '{key}' must be of type {expected_type.__name__}, "
|
|
43
|
+
f"got {type(value).__name__} instead."
|
|
44
|
+
)
|
|
20
45
|
|
|
21
|
-
|
|
46
|
+
def _validate_range(self, key: str, value: Any) -> None:
|
|
47
|
+
"""Validate the range of a setting value."""
|
|
48
|
+
if key in self.settings_ranges:
|
|
49
|
+
min_val, max_val = self.settings_ranges[key]
|
|
50
|
+
if not min_val <= value <= max_val:
|
|
51
|
+
raise ValueError(
|
|
52
|
+
f"DRF_TO_MKDOC setting '{key}' must be between {min_val} and {max_val}, "
|
|
53
|
+
f"got {value} instead."
|
|
54
|
+
)
|
|
22
55
|
|
|
56
|
+
def _validate_required(self, key: str, value: Any) -> None:
|
|
57
|
+
"""Validate if a required setting is configured."""
|
|
23
58
|
if value is None and key in self.required_settings:
|
|
24
59
|
raise ValueError(
|
|
25
60
|
f"DRF_TO_MKDOC setting '{key}' is required but not configured. "
|
|
26
61
|
f"Please add it to your Django settings under {self.user_settings_key}."
|
|
27
62
|
)
|
|
28
63
|
|
|
64
|
+
def _validate_dir(self, key: str, value: str) -> None:
|
|
65
|
+
if key not in self.path_settings or not isinstance(value, str):
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
if not value.strip():
|
|
69
|
+
raise ValueError(
|
|
70
|
+
f"DRF_TO_MKDOC path setting '{key}' cannot be empty or contain only whitespace."
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
dangerous_components = {"..", "~", "/", "\\"}
|
|
74
|
+
path_parts = Path(value).parts
|
|
75
|
+
for part in path_parts:
|
|
76
|
+
if part in dangerous_components or part.startswith("."):
|
|
77
|
+
raise ValueError(
|
|
78
|
+
f"DRF_TO_MKDOC path setting '{key}' contains unsafe path component '{part}'. "
|
|
79
|
+
f"Directory names should be simple names without separators or relative path components."
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
if Path(value).is_absolute():
|
|
83
|
+
raise ValueError(
|
|
84
|
+
f"DRF_TO_MKDOC path setting '{key}' cannot be an absolute path. "
|
|
85
|
+
f"Use relative directory names only."
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
reserved_names = {
|
|
89
|
+
"CON",
|
|
90
|
+
"PRN",
|
|
91
|
+
"AUX",
|
|
92
|
+
"NUL",
|
|
93
|
+
"COM1",
|
|
94
|
+
"COM2",
|
|
95
|
+
"COM3",
|
|
96
|
+
"COM4",
|
|
97
|
+
"COM5",
|
|
98
|
+
"COM6",
|
|
99
|
+
"COM7",
|
|
100
|
+
"COM8",
|
|
101
|
+
"COM9",
|
|
102
|
+
"LPT1",
|
|
103
|
+
"LPT2",
|
|
104
|
+
"LPT3",
|
|
105
|
+
"LPT4",
|
|
106
|
+
"LPT5",
|
|
107
|
+
"LPT6",
|
|
108
|
+
"LPT7",
|
|
109
|
+
"LPT8",
|
|
110
|
+
"LPT9",
|
|
111
|
+
}
|
|
112
|
+
if value.upper() in reserved_names:
|
|
113
|
+
raise ValueError(
|
|
114
|
+
f"DRF_TO_MKDOC path setting '{key}' uses a reserved system name '{value}'. "
|
|
115
|
+
f"Please choose a different name."
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
invalid_chars = '<>:"|?*'
|
|
119
|
+
if any(char in value for char in invalid_chars):
|
|
120
|
+
raise ValueError(
|
|
121
|
+
f"DRF_TO_MKDOC path setting '{key}' contains invalid characters. "
|
|
122
|
+
f"Avoid using: {invalid_chars}"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def get(self, key):
|
|
126
|
+
if key in self.project_settings:
|
|
127
|
+
return self.project_settings[key]
|
|
128
|
+
|
|
129
|
+
# User-provided settings take precedence
|
|
130
|
+
if key in self._user_settings:
|
|
131
|
+
value = self._user_settings[key]
|
|
132
|
+
else:
|
|
133
|
+
value = self.defaults.get(key, None)
|
|
134
|
+
|
|
135
|
+
# Run all validations
|
|
136
|
+
self._validate_required(key, value)
|
|
137
|
+
self._validate_type(key, value)
|
|
138
|
+
self._validate_range(key, value)
|
|
139
|
+
self._validate_dir(key, value)
|
|
140
|
+
|
|
29
141
|
return value
|
|
30
142
|
|
|
31
143
|
def __getattr__(self, key):
|
|
@@ -37,7 +149,7 @@ class DRFToMkDocSettings:
|
|
|
37
149
|
for setting in self.required_settings:
|
|
38
150
|
try:
|
|
39
151
|
self.get(setting)
|
|
40
|
-
except ValueError:
|
|
152
|
+
except (ValueError, AttributeError):
|
|
41
153
|
missing_settings.append(setting)
|
|
42
154
|
|
|
43
155
|
if missing_settings:
|
|
@@ -34,15 +34,16 @@ class Command(BaseCommand):
|
|
|
34
34
|
)
|
|
35
35
|
|
|
36
36
|
try:
|
|
37
|
-
#
|
|
38
|
-
self.stdout.write("
|
|
39
|
-
call_command("
|
|
40
|
-
self.stdout.write(self.style.SUCCESS("Model
|
|
37
|
+
# Extract model data from Django models
|
|
38
|
+
self.stdout.write("Extracting model data...")
|
|
39
|
+
call_command("extract_model_data", "--pretty")
|
|
40
|
+
self.stdout.write(self.style.SUCCESS("Model data extracted."))
|
|
41
41
|
|
|
42
42
|
# Generate the documentation content
|
|
43
|
-
self.stdout.write("
|
|
44
|
-
call_command("
|
|
45
|
-
|
|
43
|
+
self.stdout.write("Building documentation content...")
|
|
44
|
+
call_command("build_model_docs")
|
|
45
|
+
call_command("build_endpoint_docs")
|
|
46
|
+
self.stdout.write(self.style.SUCCESS("Documentation content built."))
|
|
46
47
|
|
|
47
48
|
# Build the MkDocs site
|
|
48
49
|
self.stdout.write("Building MkDocs site...")
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from django.core.management.base import BaseCommand
|
|
4
|
+
|
|
5
|
+
from drf_to_mkdoc.conf.settings import drf_to_mkdoc_settings
|
|
6
|
+
from drf_to_mkdoc.utils.commons.schema_utils import get_schema
|
|
7
|
+
from drf_to_mkdoc.utils.endpoint_detail_generator import (
|
|
8
|
+
generate_endpoint_files,
|
|
9
|
+
parse_endpoints_from_schema,
|
|
10
|
+
)
|
|
11
|
+
from drf_to_mkdoc.utils.endpoint_list_generator import create_endpoints_index
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Command(BaseCommand):
|
|
15
|
+
help = "Build endpoint documentation from OpenAPI schema"
|
|
16
|
+
|
|
17
|
+
def handle(self, *args, **options):
|
|
18
|
+
self.stdout.write(
|
|
19
|
+
self.style.SUCCESS("🚀 Starting endpoint documentation generation...")
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
docs_dir = self._setup_docs_directory()
|
|
23
|
+
schema_data = self._load_schema_data()
|
|
24
|
+
|
|
25
|
+
if schema_data:
|
|
26
|
+
self._generate_endpoints_documentation(schema_data, docs_dir)
|
|
27
|
+
self.stdout.write(
|
|
28
|
+
self.style.SUCCESS("✅ Endpoint documentation generation complete!")
|
|
29
|
+
)
|
|
30
|
+
else:
|
|
31
|
+
self.stdout.write(self.style.ERROR("❌ Failed to generate endpoint documentation."))
|
|
32
|
+
|
|
33
|
+
def _setup_docs_directory(self):
|
|
34
|
+
docs_dir = Path(drf_to_mkdoc_settings.DOCS_DIR)
|
|
35
|
+
docs_dir.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
return docs_dir
|
|
37
|
+
|
|
38
|
+
def _load_schema_data(self):
|
|
39
|
+
try:
|
|
40
|
+
schema = get_schema()
|
|
41
|
+
except Exception as e:
|
|
42
|
+
self.stdout.write(self.style.ERROR(f"❌ Failed to load OpenAPI schema: {e}"))
|
|
43
|
+
return {}
|
|
44
|
+
if not schema:
|
|
45
|
+
self.stdout.write(self.style.ERROR("❌ Failed to load OpenAPI schema"))
|
|
46
|
+
return {}
|
|
47
|
+
|
|
48
|
+
paths = schema.get("paths", {})
|
|
49
|
+
components = schema.get("components", {})
|
|
50
|
+
|
|
51
|
+
self.stdout.write(f"📊 Loaded {len(paths)} API paths")
|
|
52
|
+
|
|
53
|
+
return {"paths": paths, "components": components}
|
|
54
|
+
|
|
55
|
+
def _generate_endpoints_documentation(self, schema_data, docs_dir):
|
|
56
|
+
self.stdout.write("🔗 Generating endpoint documentation...")
|
|
57
|
+
|
|
58
|
+
paths = schema_data["paths"]
|
|
59
|
+
components = schema_data["components"]
|
|
60
|
+
|
|
61
|
+
endpoints_by_app = parse_endpoints_from_schema(paths)
|
|
62
|
+
total_endpoints = generate_endpoint_files(endpoints_by_app, components)
|
|
63
|
+
create_endpoints_index(endpoints_by_app, docs_dir)
|
|
64
|
+
|
|
65
|
+
self.stdout.write(
|
|
66
|
+
self.style.SUCCESS(
|
|
67
|
+
f"✅ Generated {total_endpoints} endpoint files with Django view introspection"
|
|
68
|
+
)
|
|
69
|
+
)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from django.core.management.base import BaseCommand
|
|
4
|
+
|
|
5
|
+
from drf_to_mkdoc.conf.settings import drf_to_mkdoc_settings
|
|
6
|
+
from drf_to_mkdoc.utils.commons.file_utils import load_json_data
|
|
7
|
+
from drf_to_mkdoc.utils.model_detail_generator import generate_model_docs
|
|
8
|
+
from drf_to_mkdoc.utils.model_list_generator import create_models_index
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Command(BaseCommand):
|
|
12
|
+
help = "Build model documentation from model JSON data"
|
|
13
|
+
|
|
14
|
+
def handle(self, *args, **options):
|
|
15
|
+
self.stdout.write(self.style.SUCCESS("🚀 Starting model documentation generation..."))
|
|
16
|
+
|
|
17
|
+
docs_dir = self._setup_docs_directory()
|
|
18
|
+
models_data = self._load_models_data()
|
|
19
|
+
|
|
20
|
+
if models_data:
|
|
21
|
+
self._generate_models_documentation(models_data, docs_dir)
|
|
22
|
+
self.stdout.write(self.style.SUCCESS("✅ Model documentation generation complete!"))
|
|
23
|
+
else:
|
|
24
|
+
self.stdout.write(self.style.ERROR("❌ Failed to generate model documentation."))
|
|
25
|
+
|
|
26
|
+
def _setup_docs_directory(self):
|
|
27
|
+
docs_dir = Path(drf_to_mkdoc_settings.DOCS_DIR)
|
|
28
|
+
docs_dir.mkdir(parents=True, exist_ok=True)
|
|
29
|
+
return docs_dir
|
|
30
|
+
|
|
31
|
+
def _load_models_data(self):
|
|
32
|
+
models_data = load_json_data(
|
|
33
|
+
drf_to_mkdoc_settings.MODEL_DOCS_FILE, raise_not_found=False
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
if not models_data:
|
|
37
|
+
self.stdout.write(self.style.WARNING("⚠️ No model data found"))
|
|
38
|
+
|
|
39
|
+
return models_data
|
|
40
|
+
|
|
41
|
+
def _generate_models_documentation(self, models_data, docs_dir):
|
|
42
|
+
self.stdout.write("📋 Generating model documentation...")
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
generate_model_docs(models_data)
|
|
46
|
+
create_models_index(models_data, docs_dir)
|
|
47
|
+
self.stdout.write(self.style.SUCCESS("✅ Model documentation generated"))
|
|
48
|
+
except Exception as e:
|
|
49
|
+
self.stdout.write(self.style.WARNING(f"⚠️ Failed to generate model docs: {e}"))
|
|
50
|
+
raise
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import inspect
|
|
2
2
|
import json
|
|
3
3
|
import re
|
|
4
|
+
from collections import defaultdict
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
|
|
6
7
|
from django.apps import apps
|
|
@@ -11,7 +12,7 @@ from drf_to_mkdoc.conf.settings import drf_to_mkdoc_settings
|
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
class Command(BaseCommand):
|
|
14
|
-
help = "
|
|
15
|
+
help = "Extract model data from Django model introspection and save as JSON"
|
|
15
16
|
|
|
16
17
|
def add_arguments(self, parser):
|
|
17
18
|
parser.add_argument(
|
|
@@ -38,33 +39,26 @@ class Command(BaseCommand):
|
|
|
38
39
|
|
|
39
40
|
model_docs = self.generate_model_documentation(exclude_apps)
|
|
40
41
|
|
|
41
|
-
json_data = {
|
|
42
|
-
"models": model_docs,
|
|
43
|
-
"stats": {
|
|
44
|
-
"total_models": len(model_docs),
|
|
45
|
-
"total_apps": len({model["app_label"] for model in model_docs.values()}),
|
|
46
|
-
},
|
|
47
|
-
}
|
|
48
|
-
|
|
49
42
|
output_path = Path(output_file)
|
|
50
43
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
51
44
|
with output_path.open("w", encoding="utf-8") as f:
|
|
45
|
+
payload = dict(model_docs)
|
|
52
46
|
if pretty:
|
|
53
|
-
json.dump(
|
|
47
|
+
json.dump(payload, f, ensure_ascii=False, sort_keys=True, default=str, indent=2)
|
|
54
48
|
else:
|
|
55
|
-
json.dump(
|
|
49
|
+
json.dump(payload, f, ensure_ascii=False, sort_keys=True, default=str)
|
|
56
50
|
|
|
57
51
|
self.stdout.write(
|
|
58
52
|
self.style.SUCCESS(f"✅ Generated model documentation: {output_path.absolute()}")
|
|
59
53
|
)
|
|
60
|
-
self.stdout.write(f"📊 Total models: {len(model_docs)}")
|
|
61
54
|
self.stdout.write(
|
|
62
|
-
f"
|
|
55
|
+
f"📊 Total models: {sum([len(model_docs[app_label]) for app_label in model_docs])}"
|
|
63
56
|
)
|
|
57
|
+
self.stdout.write(f"📦 Total apps: {len(model_docs)}")
|
|
64
58
|
|
|
65
59
|
def generate_model_documentation(self, exclude_apps):
|
|
66
60
|
"""Generate documentation for all Django models"""
|
|
67
|
-
model_docs =
|
|
61
|
+
model_docs = defaultdict(dict)
|
|
68
62
|
|
|
69
63
|
for app_config in apps.get_app_configs():
|
|
70
64
|
app_label = app_config.label
|
|
@@ -78,13 +72,11 @@ class Command(BaseCommand):
|
|
|
78
72
|
|
|
79
73
|
for model in app_config.get_models():
|
|
80
74
|
model_name = model.__name__
|
|
81
|
-
model_key = f"{app_label}.{model_name}"
|
|
82
|
-
|
|
83
75
|
self.stdout.write(f" 📋 Processing model: {model_name}")
|
|
84
76
|
|
|
85
|
-
model_docs[
|
|
77
|
+
model_docs[app_label][model_name] = self.introspect_model(model, app_label)
|
|
86
78
|
|
|
87
|
-
return model_docs
|
|
79
|
+
return {app_label: dict(models) for app_label, models in model_docs.items()}
|
|
88
80
|
|
|
89
81
|
def introspect_model(self, model, app_label):
|
|
90
82
|
"""Introspect a single Django model"""
|
|
@@ -154,6 +146,9 @@ class Command(BaseCommand):
|
|
|
154
146
|
"type": field.__class__.__name__,
|
|
155
147
|
"related_model": related_model_label,
|
|
156
148
|
"related_name": getattr(field, "related_name", None),
|
|
149
|
+
"app_label": field.related_model._meta.app_label,
|
|
150
|
+
"table_name": field.related_model._meta.db_table,
|
|
151
|
+
"verbose_name": field.related_model._meta.verbose_name,
|
|
157
152
|
"on_delete": self.get_on_delete_name(field),
|
|
158
153
|
"null": getattr(field, "null", False),
|
|
159
154
|
"blank": getattr(field, "blank", False),
|
|
@@ -286,7 +281,7 @@ class Command(BaseCommand):
|
|
|
286
281
|
if display_method_match:
|
|
287
282
|
field_name = display_method_match.group(1)
|
|
288
283
|
if field_name in model_field_names:
|
|
289
|
-
# Exclude
|
|
284
|
+
# Exclude built-in get_<field>_display for fields present on the model
|
|
290
285
|
continue
|
|
291
286
|
|
|
292
287
|
method = getattr(model, attr_name)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<!-- inject CSS and JS directly -->
|
|
2
|
+
{% load custom_filters %}
|
|
3
|
+
{% for stylesheet in stylesheets %}
|
|
4
|
+
<link rel="stylesheet" href="{% static_with_prefix stylesheet prefix_path %}">
|
|
5
|
+
{% endfor %}
|
|
6
|
+
{% for script in scripts %}
|
|
7
|
+
<script src="{% static_with_prefix script prefix_path %}" defer></script>
|
|
8
|
+
{% endfor %}
|
|
9
|
+
|
|
10
|
+
# {{ method|upper }} {{ path }}
|
|
11
|
+
|
|
12
|
+
{{ method|format_method_badge|safe }} `{{ path }}`
|
|
13
|
+
|
|
14
|
+
**View class:** {{ viewset_name }}
|
|
15
|
+
|
|
16
|
+
{% if summary %}
|
|
17
|
+
## Overview
|
|
18
|
+
|
|
19
|
+
{{ summary }}
|
|
20
|
+
{% endif %}
|
|
21
|
+
|
|
22
|
+
{% if operation_id %}
|
|
23
|
+
**Operation ID:** `{{ operation_id }}`
|
|
24
|
+
{% endif %}
|
|
25
|
+
|
|
26
|
+
{% if description %}
|
|
27
|
+
{{ description|safe }}
|
|
28
|
+
{% endif %}
|
|
29
|
+
|
|
30
|
+
{% include "endpoints/detail/path_parameters.html" %}
|
|
31
|
+
{% include "endpoints/detail/query_parameters.html" %}
|
|
32
|
+
{% include "endpoints/detail/request_body.html" %}
|
|
33
|
+
{% include "endpoints/detail/responses.html" %}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{% if path_params %}
|
|
2
|
+
## Path Parameters
|
|
3
|
+
|
|
4
|
+
| Name | Type | Required | Description |
|
|
5
|
+
|------|------|----------|-------------|
|
|
6
|
+
{% for param in path_params %}| `{{ param.name }}` | `{{ param.schema.type|default:"string" }}` | {{ param.required|yesno:"Yes,No" }} | {{ param.description }} |{% endfor %}
|
|
7
|
+
{% endif %}
|
|
8
|
+
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{% comment %}
|
|
2
|
+
Check if any query parameter fields have content
|
|
3
|
+
{% endcomment %}
|
|
4
|
+
|
|
5
|
+
{% if query_parameters.filter_fields or query_parameters.search_fields or query_parameters.ordering_fields or query_parameters.filter_backends or query_parameters.pagination_fields %}
|
|
6
|
+
## Query Parameters
|
|
7
|
+
|
|
8
|
+
{% if query_parameters.filter_fields %}
|
|
9
|
+
### Filter Fields
|
|
10
|
+
{% for field in query_parameters.filter_fields %}
|
|
11
|
+
- `{{ field }}`
|
|
12
|
+
{% endfor %}
|
|
13
|
+
{% endif %}
|
|
14
|
+
|
|
15
|
+
{% if query_parameters.search_fields %}
|
|
16
|
+
### Search Fields
|
|
17
|
+
{% for field in query_parameters.search_fields %}
|
|
18
|
+
- `{{ field }}`
|
|
19
|
+
{% endfor %}
|
|
20
|
+
{% endif %}
|
|
21
|
+
|
|
22
|
+
{% if query_parameters.ordering_fields %}
|
|
23
|
+
### Ordering Fields
|
|
24
|
+
{% for field in query_parameters.ordering_fields %}
|
|
25
|
+
- `{{ field }}`
|
|
26
|
+
{% endfor %}
|
|
27
|
+
{% endif %}
|
|
28
|
+
|
|
29
|
+
{% if query_parameters.filter_backends %}
|
|
30
|
+
### Filter Backends
|
|
31
|
+
{% for backend in query_parameters.filter_backends %}
|
|
32
|
+
- `{{ backend }}`
|
|
33
|
+
{% endfor %}
|
|
34
|
+
{% endif %}
|
|
35
|
+
|
|
36
|
+
{% if query_parameters.pagination_fields %}
|
|
37
|
+
### Pagination Fields
|
|
38
|
+
{% for field in query_parameters.pagination_fields %}
|
|
39
|
+
- `{{ field }}`
|
|
40
|
+
{% endfor %}
|
|
41
|
+
{% endif %}
|
|
42
|
+
|
|
43
|
+
{% endif %}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{% load custom_filters %}
|
|
2
|
+
|
|
3
|
+
{% if responses %}
|
|
4
|
+
## Responses
|
|
5
|
+
|
|
6
|
+
{% for response in responses %}
|
|
7
|
+
### {{ response.status_code }}
|
|
8
|
+
|
|
9
|
+
{% if response.description %}
|
|
10
|
+
{{ response.description }}
|
|
11
|
+
{% endif %}
|
|
12
|
+
|
|
13
|
+
{% if response.example %}
|
|
14
|
+
{{ response.example|format_json }}
|
|
15
|
+
{% endif %}
|
|
16
|
+
|
|
17
|
+
{% endfor %}
|
|
18
|
+
{% endif %}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# API Endpoints
|
|
2
|
+
|
|
3
|
+
<style>
|
|
4
|
+
{% for stylesheet in stylesheets %}
|
|
5
|
+
@import url("{{ stylesheet|safe }}");
|
|
6
|
+
{% endfor %}
|
|
7
|
+
</style>
|
|
8
|
+
|
|
9
|
+
{% for script in scripts %}
|
|
10
|
+
<script src="{{ script|safe }}" defer></script>
|
|
11
|
+
{% endfor %}
|
|
12
|
+
|
|
13
|
+
<div class="main-content">
|
|
14
|
+
{% include "endpoints/list/filter_section.html" %}
|
|
15
|
+
{% for app_name, endpoints in endpoints_by_app.items %}
|
|
16
|
+
<h2>{{ app_name|title }}</h2>
|
|
17
|
+
<div class="endpoints-grid">
|
|
18
|
+
{% for endpoint in endpoints %}
|
|
19
|
+
{% include "endpoints/list/endpoint_card.html" %}
|
|
20
|
+
{% endfor %}
|
|
21
|
+
</div>
|
|
22
|
+
{% endfor %}
|
|
23
|
+
</div>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{% load custom_filters %}
|
|
2
|
+
<a href="{{ endpoint.link_url }}" class="endpoint-card"
|
|
3
|
+
data-method="{{ endpoint.method|lower }}"
|
|
4
|
+
data-path="{{ endpoint.path|lower }}"
|
|
5
|
+
data-app="{{ app_name|lower }}"
|
|
6
|
+
data-auth="{{ endpoint.auth_required|default:'false'|lower }}"
|
|
7
|
+
data-pagination="{{ endpoint.pagination_support|default:'false'|lower }}"
|
|
8
|
+
data-search="{{ endpoint.view_class.search_fields|default:''|length|yesno:'true,false'|lower }}"
|
|
9
|
+
data-ordering="{{ endpoint.ordering_support|default:'false'|lower }}"
|
|
10
|
+
data-models="{{ endpoint.related_models|default:''|join:' '|lower }}"
|
|
11
|
+
data-roles="{{ endpoint.permission_roles|default:''|join:' '|lower }}"
|
|
12
|
+
data-content-type="{{ endpoint.content_type|default:''|lower }}"
|
|
13
|
+
data-tags="{{ endpoint.tags|default:''|join:' '|lower }}"
|
|
14
|
+
data-schema="{{ endpoint.schema_fields|default:''|join:' '|lower }}"
|
|
15
|
+
data-params="{{ endpoint.query_parameters|default:''|join:' '|lower }}">
|
|
16
|
+
<span class="method-badge method-{{ endpoint.method|lower }}">{{ endpoint.method }}</span>
|
|
17
|
+
<span class="endpoint-path">{{ endpoint.path }}</span>
|
|
18
|
+
</a>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<div class="filter-sidebar collapsed" id="filterSidebar">
|
|
2
|
+
<h3 class="filter-title">🔍 Filters</h3>
|
|
3
|
+
<div class="filter-grid">
|
|
4
|
+
{% for filter_key in active_filters %}
|
|
5
|
+
{% include "endpoints/list/filters/"|add:filter_key|add:".html" %}
|
|
6
|
+
{% endfor %}
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<div class="filter-actions">
|
|
10
|
+
<button class="filter-apply" onclick="applyFilters()">Apply</button>
|
|
11
|
+
<button class="filter-clear" onclick="clearFilters()">Clear</button>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<div class="filter-results">Showing 0 endpoints</div>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<div class="filter-group">
|
|
2
|
+
<label class="filter-label">HTTP Method</label>
|
|
3
|
+
<select id="filter-method" class="filter-select">
|
|
4
|
+
<option value="">All</option>
|
|
5
|
+
<option value="get">GET</option>
|
|
6
|
+
<option value="post">POST</option>
|
|
7
|
+
<option value="put">PUT</option>
|
|
8
|
+
<option value="patch">PATCH</option>
|
|
9
|
+
<option value="delete">DELETE</option>
|
|
10
|
+
</select>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# {{ name }}
|
|
2
|
+
|
|
3
|
+
<!-- inject CSS directly -->
|
|
4
|
+
{% for stylesheet in stylesheets %}
|
|
5
|
+
<link rel="stylesheet" href="{{ stylesheet }}">
|
|
6
|
+
{% endfor %}
|
|
7
|
+
|
|
8
|
+
**App:** {{ app_label }}
|
|
9
|
+
|
|
10
|
+
**Table:** `{{ table_name }}`
|
|
11
|
+
|
|
12
|
+
## Description
|
|
13
|
+
|
|
14
|
+
{{ description }}
|
|
15
|
+
|
|
16
|
+
{% if fields %}
|
|
17
|
+
{% include "model_detail/fields.html" with fields=fields %}
|
|
18
|
+
{% endif %}
|
|
19
|
+
|
|
20
|
+
{% if has_choices %}
|
|
21
|
+
{% include "model_detail/choices.html" with fields=fields %}
|
|
22
|
+
{% endif %}
|
|
23
|
+
|
|
24
|
+
{% if relationships %}
|
|
25
|
+
{% include "model_detail/relationships.html" with relationships=relationships %}
|
|
26
|
+
{% endif %}
|
|
27
|
+
|
|
28
|
+
{% if methods %}
|
|
29
|
+
{% include "model_detail/methods.html" with methods=methods %}
|
|
30
|
+
{% endif %}
|
|
31
|
+
|
|
32
|
+
{% if meta_options %}
|
|
33
|
+
{% include "model_detail/meta.html" with meta_options=meta_options %}
|
|
34
|
+
{% endif %}
|