feat(app): allow creating multiple Django apps with comma or space separated names and improve project structure detection

This commit is contained in:
sankalp committed 2025-10-29 11:41:31 +05:45
1 parent 53376e13d6
commit 46a5fee20f
2 files changed
+121 -48

No files matched your search

+59 -31
View File
@@ -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:
+62 -17
View File
@@ -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("")