From f82bf1172deaecac5e961539f20b18446dbe8e95 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sat, 31 Jan 2026 02:15:50 +0545 Subject: [PATCH] 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. --- djinit/services/app.py | 55 +++++++++++++++++++++++++++++++++----- djinit/services/files.py | 18 +++++++++++-- djinit/services/project.py | 47 +++++++++++++++++++++++++++++--- 3 files changed, 107 insertions(+), 13 deletions(-) diff --git a/djinit/services/app.py b/djinit/services/app.py index 660673c..23c18f8 100644 --- a/djinit/services/app.py +++ b/djinit/services/app.py @@ -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"), diff --git a/djinit/services/files.py b/djinit/services/files.py index c400af1..8dac659 100644 --- a/djinit/services/files.py +++ b/djinit/services/files.py @@ -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" diff --git a/djinit/services/project.py b/djinit/services/project.py index 9cc0f98..7d57071 100644 --- a/djinit/services/project.py +++ b/djinit/services/project.py @@ -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