diff --git a/src/scripts/app_generator.py b/src/scripts/app_generator.py index 064cf6a..ab20b30 100644 --- a/src/scripts/app_generator.py +++ b/src/scripts/app_generator.py @@ -171,43 +171,71 @@ class AppManager: return None def _detect_project_structure(self) -> tuple[bool, Optional[str], str]: - """ - Detect if the project uses nested apps and return the structure info. - - Returns: - tuple: (is_nested, nested_dir, apps_base_dir) - """ - # Look for project directories that contain settings + # Step 1: find project directory containing settings/base.py + project_dir: Optional[str] = None + settings_base_path: Optional[str] = None for item in os.listdir(self.current_dir): if os.path.isdir(item) and not item.startswith(".") and item != "__pycache__": - # Check if this directory contains Django project files - project_path = os.path.join(self.current_dir, item) - settings_path = os.path.join(project_path, "settings") + candidate = os.path.join(self.current_dir, item) + base_py = os.path.join(candidate, "settings", "base.py") + if os.path.exists(base_py): + project_dir = candidate + settings_base_path = base_py + break - # Check if settings directory exists with base.py - if os.path.exists(settings_path) and os.path.exists(os.path.join(settings_path, "base.py")): - # Found the project directory, now check for nested apps - project_name = item - - # Look for common nested app directories - for nested_dir in ["apps", "modules", "packages"]: - nested_path = os.path.join(self.current_dir, nested_dir) - if os.path.exists(nested_path) and os.path.isdir(nested_path): - # Check if there are any Django apps in this directory - for app_item in os.listdir(nested_path): - app_path = os.path.join(nested_path, app_item) - if os.path.isdir(app_path) and os.path.exists(os.path.join(app_path, "apps.py")): - # Found nested apps structure - return True, nested_dir, nested_path - - # No nested structure found - return False, None, self.current_dir + # Fallback: current_dir/settings/base.py + if project_dir is None: + base_py = os.path.join(self.current_dir, "settings", "base.py") + if os.path.exists(base_py): + project_dir = self.current_dir + settings_base_path = base_py - # Fallback: check current directory - settings_path = os.path.join(self.current_dir, "settings") - if os.path.exists(settings_path) and os.path.exists(os.path.join(settings_path, "base.py")): + if project_dir is None: + # Not a Django project structure return False, None, self.current_dir + # Step 2: try to infer nested_dir from USER_DEFINED_APPS in base.py + try: + if settings_base_path and os.path.exists(settings_base_path): + with open(settings_base_path, "r") as f: + base_content = f.read() + # Find lines inside USER_DEFINED_APPS and extract first dotted app + in_user_apps = False + for line in base_content.splitlines(): + if "USER_DEFINED_APPS" in line and "=" in line: + in_user_apps = True + continue + if in_user_apps and "]" in line: + break + if in_user_apps: + line_stripped = line.strip().strip(",") + if line_stripped.startswith("\"") or line_stripped.startswith("'"): + app_label = line_stripped.strip("\"'") + if "." in app_label: + prefix = app_label.split(".", 1)[0] + nested_path = os.path.join(self.current_dir, prefix) + if os.path.isdir(nested_path): + return True, prefix, nested_path + except Exception: + # Ignore parsing issues and continue with heuristics + pass + + # Step 3: look for existing package directories in project root + preferred = ["apps", "modules", "packages"] + for name in preferred: + nested_path = os.path.join(self.current_dir, name) + if os.path.isdir(nested_path) and os.path.exists(os.path.join(nested_path, "__init__.py")): + return True, name, nested_path + + # Any other directory with __init__.py qualifies as a package + for item in os.listdir(self.current_dir): + if item in ("__pycache__",) or item.startswith("."): + continue + nested_path = os.path.join(self.current_dir, item) + if os.path.isdir(nested_path) and os.path.exists(os.path.join(nested_path, "__init__.py")): + return True, item, nested_path + + # Step 4: fallback return False, None, self.current_dir def _create_additional_files(self) -> bool: diff --git a/src/scripts/command_handler.py b/src/scripts/command_handler.py index 7d862bb..2710c81 100644 --- a/src/scripts/command_handler.py +++ b/src/scripts/command_handler.py @@ -26,8 +26,20 @@ def parse_arguments() -> argparse.Namespace: secret_parser.add_argument("--length", type=int, default=50, help="Length of each secret key") # App command - app_parser = subparsers.add_parser("app", help="Create a new Django app") - app_parser.add_argument("app_name", help="Name of the Django app to create") + app_parser = subparsers.add_parser( + "app", help="Create one or more Django apps (comma or space separated)" + ) + app_parser.add_argument( + "app_name", + nargs="+", + help=( + "App name(s) or comma-separated list. Examples: \n" + " djinit app users \n" + " djinit app users,products,orders \n" + " djinit app users products orders \n" + " djinit app users, products, orders" + ), + ) return parser.parse_args() @@ -67,35 +79,68 @@ def handle_app_command(args: argparse.Namespace) -> None: UIFormatter.print_header("Django App Creation") UIFormatter.print_info("") - # Validate app name - is_valid, error_msg = validate_app_name(args.app_name) - if not is_valid: - UIFormatter.print_error(error_msg) + # Parse comma-separated names and validate + # args.app_name is a list due to nargs="+"; split each token by comma + tokens = args.app_name if isinstance(args.app_name, list) else [args.app_name] + pieces = [] + for token in tokens: + if token is None: + continue + for part in str(token).split(","): + part = part.strip() + if part: + pieces.append(part) + app_names = pieces + + # If no comma, keep single name behavior + if not app_names: + UIFormatter.print_error("App name cannot be empty") sys.exit(1) - # Create app manager - app_manager = AppManager(args.app_name) + # Deduplicate while preserving order + seen = set() + deduped_names = [] + for name in app_names: + if name not in seen: + deduped_names.append(name) + seen.add(name) - # Create the app - success = app_manager.create_app() + # Validate all names first + for name in deduped_names: + is_valid, error_msg = validate_app_name(name) + if not is_valid: + UIFormatter.print_error(f"Invalid app name '{name}': {error_msg}") + sys.exit(1) - if not success: - UIFormatter.print_error(f"Failed to create Django app '{args.app_name}'") + # Create apps sequentially + any_failure = False + for name in deduped_names: + app_manager = AppManager(name) + success = app_manager.create_app() + if not success: + UIFormatter.print_error(f"Failed to create Django app '{name}'") + any_failure = True + break + UIFormatter.print_success(f"Django app '{name}' created successfully!") + + if any_failure: sys.exit(1) - UIFormatter.print_success(f"Django app '{args.app_name}' created successfully!") - # Show next steps from rich.panel import Panel from rich.text import Text instructions = Text() instructions.append("🚀 Next Steps:\n", style=UIColors.ACCENT) - instructions.append("1. The app has been added to INSTALLED_APPS in base.py\n") - instructions.append("2. Create your models in the app's models.py\n") + instructions.append( + "1. The app(s) have been added to INSTALLED_APPS in settings/base.py\n" + ) + instructions.append("2. Create your models in each app's models.py\n") instructions.append("3. Run migrations: python manage.py makemigrations\n") instructions.append("4. Apply migrations: python manage.py migrate\n") - instructions.append("5. Create views, serializers and routes(URLs) for your app\n") + instructions.append( + "5. Create views, serializers and routes(URLs) for your app(s)\n" + ) console.print(Panel(instructions, title="💡 What's Next", border_style="green")) UIFormatter.print_info("")