feat(src/djinit/creators): add unified URL registration and AppConfig path handling
- New Features: Automatically include app URLs in project urls.py and api/v1/urls.py; Generate full AppConfig paths using a unified utility - Enhancements: Improved nested-app detection and path resolution across predefined, standard, single, and unified structures; Refined context transformation for base settings; Updated URL template rendering - Bug Fixes: Fixed duplicate include‑statement handling; Corrected URL update error paths and exception safety
This commit is contained in:
3 files changed
+77
-43
No files matched your search
+66
-24
@@ -47,8 +47,11 @@ class AppCreator(BaseService):
|
||||
if not self._add_to_installed_apps():
|
||||
return False
|
||||
|
||||
# Register URLs based on structure
|
||||
if structure_type == "predefined":
|
||||
self._add_to_api_v1_urls(self.app_name)
|
||||
elif structure_type in ["standard", "single"]:
|
||||
self._add_to_project_urls(self.app_name, structure_type)
|
||||
|
||||
UIFormatter.print_success(f"Django app '{self.app_name}' created and configured successfully!")
|
||||
return True
|
||||
@@ -165,10 +168,22 @@ class AppCreator(BaseService):
|
||||
# Calculate the app module path based on structure
|
||||
app_module_path = self._calculate_app_module_path(structure_type)
|
||||
|
||||
# Use full AppConfig path to properly support nested apps
|
||||
# e.g. apps.users -> apps.users.apps.UsersConfig
|
||||
app_config_name = self.app_name.title().replace("_", "")
|
||||
full_app_config = f"{app_module_path}.apps.{app_config_name}Config"
|
||||
# Use centralized logic for AppConfig path
|
||||
is_nested, nested_dir, _ = self._get_project_structure()
|
||||
nested = structure_type in ["unified", "predefined"] or (structure_type == "standard" and is_nested)
|
||||
|
||||
# Determine the effective nested_dir for config path
|
||||
if structure_type in ["unified", "predefined"]:
|
||||
effective_nested_dir = "apps"
|
||||
elif structure_type == "single":
|
||||
# For single structure, app is inside the project module
|
||||
project_dir, _ = CommonUtils.find_project_dir(self.current_dir)
|
||||
effective_nested_dir = os.path.basename(project_dir) if project_dir else None
|
||||
nested = True
|
||||
else:
|
||||
effective_nested_dir = nested_dir
|
||||
|
||||
full_app_config = CommonUtils.get_full_app_config_path(self.app_name, nested, effective_nested_dir)
|
||||
|
||||
# We'll check if either the module path or the full config path exists
|
||||
# but we'll prefer adding the full config path
|
||||
@@ -281,36 +296,63 @@ class AppCreator(BaseService):
|
||||
|
||||
return success
|
||||
|
||||
def _add_to_api_v1_urls(self, app_name: str) -> None:
|
||||
api_v1_urls = os.path.join(self.current_dir, "api", "v1", "urls.py")
|
||||
if not os.path.exists(api_v1_urls):
|
||||
def _add_to_project_urls(self, app_name: str, structure_type: str) -> None:
|
||||
"""Add app URLs to the main project urls.py."""
|
||||
project_dir, settings_base_path = CommonUtils.find_project_dir(self.current_dir)
|
||||
if not project_dir:
|
||||
return
|
||||
|
||||
project_urls = os.path.join(project_dir, "urls.py")
|
||||
if not os.path.exists(project_urls):
|
||||
return
|
||||
|
||||
app_module_path = self._calculate_app_module_path(structure_type)
|
||||
include_stmt = f'path("", include("{app_module_path}.urls")),'
|
||||
|
||||
try:
|
||||
with open(api_v1_urls, encoding="utf-8") as f:
|
||||
with open(project_urls, encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
import_line = "from django.urls import include, path"
|
||||
if import_line not in content:
|
||||
# ensure base import exists (fallback)
|
||||
content = import_line + "\n\n" + content
|
||||
|
||||
include_stmt = f'path("{app_name}/", include("apps.{app_name}.urls")),'
|
||||
if include_stmt in content:
|
||||
return
|
||||
|
||||
if "urlpatterns = [" in content:
|
||||
parts = content.split("urlpatterns = [", 1)
|
||||
before = parts[0]
|
||||
rest = parts[1]
|
||||
if "]" in rest:
|
||||
idx = rest.rfind("]")
|
||||
new_rest = rest[:idx]
|
||||
if not new_rest.endswith("\n"):
|
||||
new_rest += "\n"
|
||||
new_rest += f" {include_stmt}\n" + rest[idx:]
|
||||
content = before + "urlpatterns = [" + new_rest
|
||||
# Find the best place to insert (before the closing bracket of urlpatterns)
|
||||
idx = content.rfind("]")
|
||||
if idx != -1:
|
||||
content = content[:idx] + f" {include_stmt}\n" + content[idx:]
|
||||
|
||||
with open(project_urls, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
UIFormatter.print_success(f"Added '{app_name}' URLs to {os.path.basename(project_urls)}")
|
||||
except Exception as e:
|
||||
UIFormatter.print_warning(f"Could not automatically update {os.path.basename(project_urls)}: {e}")
|
||||
|
||||
def _add_to_api_v1_urls(self, app_name: str) -> None:
|
||||
api_v1_urls = os.path.join(self.current_dir, "api", "v1", "urls.py")
|
||||
if not os.path.exists(api_v1_urls):
|
||||
return
|
||||
|
||||
structure_type = self._get_structure_type()
|
||||
app_module_path = self._calculate_app_module_path(structure_type)
|
||||
|
||||
try:
|
||||
with open(api_v1_urls, encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
include_stmt = f'path("{app_name}/", include("{app_module_path}.urls")),'
|
||||
if include_stmt in content:
|
||||
return
|
||||
|
||||
if "urlpatterns = [" in content:
|
||||
idx = content.rfind("]")
|
||||
if idx != -1:
|
||||
content = content[:idx] + f" {include_stmt}\n" + content[idx:]
|
||||
|
||||
with open(api_v1_urls, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
UIFormatter.print_success(f"Added '{app_name}' URLs to api/v1/urls.py")
|
||||
except Exception:
|
||||
return
|
||||
@@ -81,16 +81,12 @@ class FileCreator(BaseService):
|
||||
# Prepare context for base.py with full AppConfig paths
|
||||
base_settings_context = base_context.copy()
|
||||
if "app_names" in base_settings_context:
|
||||
transformed_apps = []
|
||||
for app in base_settings_context["app_names"]:
|
||||
# Transform to full AppConfig path: module.path -> module.path.apps.ConfigName
|
||||
short_name = app.split(".")[-1]
|
||||
config_name = short_name.title().replace("_", "") + "Config"
|
||||
# If it's the special "apps" container in unified structure, we might want to skip or handle differently
|
||||
# But assumes all user apps follow standard djinit structure
|
||||
full_path = f"{app}.apps.{config_name}"
|
||||
transformed_apps.append(full_path)
|
||||
base_settings_context["app_names"] = transformed_apps
|
||||
nested = bool(self.metadata.get("nested_apps"))
|
||||
nested_dir = self.metadata.get("nested_dir")
|
||||
base_settings_context["app_names"] = [
|
||||
CommonUtils.get_full_app_config_path(name, nested, nested_dir)
|
||||
for name in base_settings_context["app_names"]
|
||||
]
|
||||
|
||||
for filename, context in [
|
||||
("base.py", base_settings_context),
|
||||
@@ -159,7 +155,7 @@ class FileCreator(BaseService):
|
||||
def create_env_file(self) -> None:
|
||||
"""Create .env.sample file with environment variables."""
|
||||
context = {
|
||||
"project_name": self.module_name, # Use module_name here for settings path
|
||||
"project_name": self.module_name, # Use module_name here for settings path
|
||||
"use_database_url": self.metadata.get("use_database_url", True),
|
||||
"database_type": self.metadata.get("database_type", "postgresql"),
|
||||
}
|
||||
@@ -245,7 +241,7 @@ class FileCreator(BaseService):
|
||||
CommonUtils.create_file_from_template(
|
||||
api_v1_urls_path,
|
||||
"config/urls.py-tpl",
|
||||
{"url_type": "api_version", "app_name": "v1", "app_list": [], "app_module": "apps"},
|
||||
{"url_type": "api_version", "app_name": "v1", "app_list": self.app_names, "app_module": "apps"},
|
||||
"Created api/v1/urls.py",
|
||||
)
|
||||
|
||||
|
||||
@@ -70,14 +70,10 @@ class ProjectCreator(BaseService):
|
||||
|
||||
raise ConfigError("Could not read base.py settings file")
|
||||
|
||||
nested = bool(self.metadata.get("nested_apps"))
|
||||
nested_dir = self.metadata.get("nested_dir")
|
||||
app_module_paths = CommonUtils.calculate_app_module_paths(self.app_names, self.metadata)
|
||||
|
||||
# Transform to full AppConfig paths (e.g. apps.users.apps.UsersConfig)
|
||||
full_config_paths = []
|
||||
for app in app_module_paths:
|
||||
short_name = app.split(".")[-1]
|
||||
config_name = short_name.title().replace("_", "") + "Config"
|
||||
full_config_paths.append(f"{app}.apps.{config_name}")
|
||||
full_config_paths = [CommonUtils.get_full_app_config_path(name, nested, nested_dir) for name in self.app_names]
|
||||
|
||||
existing_apps = CommonUtils.extract_existing_apps(content)
|
||||
|
||||
|
||||
Reference in new issue
Block a user