feat(djinit/services): support nested apps and full AppConfig path handling

- New Features
  - Detect and process nested project structures, generating correct module paths for AppConfig.
  - Automatically upgrade legacy app configurations from simple module paths to full AppConfig paths.
- Enhancements
  - Insert full AppConfig paths into USER_DEFINED_APPS, improving template rendering for all settings files.
  - Provide clearer success and upgrade messages to users.
- Bug Fixes
  - Avoid duplicate app entries and handle missing USER_DEFINED_APPS sections without crashing.
  - Ensure proper __init__.py creation in nested directories.
This commit is contained in:
sankalp committed 2026-01-31 02:15:50 +05:45
1 parent cab69c2232
commit f82bf1172d
3 files changed
+107 -13

No files matched your search

+48 -7
View File
@@ -78,8 +78,17 @@ class AppManager:
if self._is_predefined_structure():
return self._create_predefined_app(os.path.join(self.current_dir, "apps"))
_, _, apps_base_dir = self._get_project_structure()
success = DjangoHelper.startapp(self.app_name, apps_base_dir)
is_nested, nested_dir, apps_base_dir = self._get_project_structure()
# Calculate app_module based on project structure
if is_nested and nested_dir:
# Normalize nested_dir (replace / with . for module path)
module_base = nested_dir.replace(os.path.sep, '.')
app_module = f"{module_base}.{self.app_name}"
else:
app_module = self.app_name
success = DjangoHelper.startapp(self.app_name, apps_base_dir, app_module)
if success:
UIFormatter.print_success(f"Created Django app '{self.app_name}' in {apps_base_dir}")
else:
@@ -106,23 +115,49 @@ class AppManager:
is_nested, nested_dir, _ = self._get_project_structure()
app_module_path = calculate_app_module_path(self.app_name, is_nested, nested_dir)
# 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"
# We'll check if either the module path or the full config path exists
# but we'll prefer adding the full config path
existing_apps = extract_existing_apps(content)
if app_module_path in existing_apps:
UIFormatter.print_success(f"App '{app_module_path}' already configured in USER_DEFINED_APPS")
# Check if full config is already there
if full_app_config in existing_apps:
UIFormatter.print_success(f"App '{full_app_config}' already configured in USER_DEFINED_APPS")
return True
# Check if legacy module path is there, if so, upgrade it
if app_module_path in existing_apps:
from djinit.utils.common import replace_app_in_user_defined_apps
UIFormatter.print_info(f"Upgrading app configuration from '{app_module_path}' to '{full_app_config}'...")
updated_content = replace_app_in_user_defined_apps(content, app_module_path, full_app_config)
if updated_content:
with open(base_settings_path, "w") as f:
f.write(updated_content)
UIFormatter.print_success(f"Upgraded '{app_module_path}' to '{full_app_config}' in USER_DEFINED_APPS")
return True
else:
UIFormatter.print_error(f"Failed to upgrade app '{app_module_path}'")
return False
if "USER_DEFINED_APPS" not in content:
UIFormatter.print_error("Could not find USER_DEFINED_APPS section in base.py")
return False
updated_content = insert_apps_into_user_defined_apps(content, [app_module_path])
updated_content = insert_apps_into_user_defined_apps(content, [full_app_config])
if not updated_content:
return False
with open(base_settings_path, "w") as f:
f.write(updated_content)
UIFormatter.print_success(f"Added '{app_module_path}' to USER_DEFINED_APPS in base.py")
UIFormatter.print_success(f"Added '{full_app_config}' to USER_DEFINED_APPS in base.py")
return True
def _get_project_structure(self) -> tuple[bool, Optional[str], str]:
@@ -192,7 +227,13 @@ class AppManager:
model_class_name = "".join([part.capitalize() for part in self.app_name.split("_")])
app_module = f"apps.{self.app_name}"
base_context = {"app_name": self.app_name, "app_module": app_module, "model_class_name": model_class_name}
app_config_name = self.app_name.title().replace('_', '')
base_context = {
"app_name": self.app_name,
"app_config_name": app_config_name,
"app_module": app_module,
"model_class_name": model_class_name,
}
create_file_from_template(
os.path.join(app_dir, "apps.py"),
+16 -2
View File
@@ -75,10 +75,24 @@ class FileManager:
secret_key = generate_secret_key()
dev_context = {"secret_key": secret_key}
# 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
for filename, context in [
("base.py", base_context),
("base.py", base_settings_context),
("development.py", dev_context),
("production.py", base_context),
("production.py", base_settings_context),
]:
filepath = os.path.join(settings_dir, filename)
template_path = f"config/settings/{filename.replace('.py', '')}.py-tpl"
+43 -4
View File
@@ -46,8 +46,12 @@ class ProjectManager:
if apps_base_dir != self.project_root:
create_directory_with_init(apps_base_dir, f"Created {os.path.basename(apps_base_dir)}/__init__.py")
for app_name in self.app_names:
DjangoHelper.startapp(app_name, apps_base_dir)
# Calculate module paths for correct AppConfig generation (e.g. src.users)
app_module_paths = calculate_app_module_paths(self.app_names, self.metadata)
for i, app_name in enumerate(self.app_names):
app_module = app_module_paths[i]
DjangoHelper.startapp(app_name, apps_base_dir, app_module)
UIFormatter.print_success(f"Django app '{app_name}' created successfully!")
self.add_apps_to_settings()
@@ -69,10 +73,45 @@ class ProjectManager:
app_module_paths = calculate_app_module_paths(self.app_names, self.metadata)
existing_apps = extract_existing_apps(content)
apps_to_add = [app for app in app_module_paths if app not in existing_apps]
# 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}")
existing_apps = extract_existing_apps(content)
# Check against both module path and full config path to avoid duplicates
apps_to_add = []
for i, full_path in enumerate(full_config_paths):
mod_path = app_module_paths[i]
if full_path not in existing_apps and mod_path not in existing_apps:
apps_to_add.append(full_path)
elif mod_path in existing_apps:
# If legacy module path exists, we could upgrade it here,
# but ProjectManager is typically initialized on creation.
# For now, let's just not add a duplicate.
# Actually, let's upgrade it if we can
pass
# If we found apps to add, insert them
if not apps_to_add:
# Check if we need to upgrade any legacy paths
upgraded = False
for i, full_path in enumerate(full_config_paths):
mod_path = app_module_paths[i]
if mod_path in existing_apps and full_path not in existing_apps:
from djinit.utils.common import replace_app_in_user_defined_apps
content = replace_app_in_user_defined_apps(content, mod_path, full_path)
upgraded = True
if upgraded:
with open(base_settings_path, "w") as f:
f.write(content)
UIFormatter.print_success("Upgraded existing apps to full AppConfig paths")
return
UIFormatter.print_success("All apps already configured in USER_DEFINED_APPS")
return