feat(app): allow creating multiple Django apps with comma or space separated names and improve project structure detection
This commit is contained in:
2 files changed
+121
-48
No files matched your search
@@ -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:
|
||||
|
||||
@@ -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("")
|
||||
Reference in new issue
Block a user