338 lines
12 KiB
Python
338 lines
12 KiB
Python
"""
|
|
User input handling for Django project setup.
|
|
Handles collection and processing of user inputs during setup.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
from typing import Tuple, TypedDict
|
|
|
|
import questionary
|
|
|
|
from djinit.core.types import ProjectMetadata
|
|
from djinit.ui.console import UIColors, UIFormatter, console
|
|
from djinit.utils.common import get_package_name
|
|
from djinit.utils.validators import validate_app_name, validate_project_name
|
|
|
|
|
|
class StructureOptions(TypedDict):
|
|
project_dir: str
|
|
predefined: bool
|
|
unified: bool
|
|
single: bool
|
|
single_module_name: str | None
|
|
database_type: str
|
|
use_database_url: bool
|
|
use_github: bool
|
|
use_gitlab: bool
|
|
|
|
|
|
class InputCollector:
|
|
def __init__(self):
|
|
pass
|
|
|
|
def get_validated_input(self, prompt: str, validator: callable, input_type: str, allow_empty: bool = False) -> str:
|
|
def q_validator(text):
|
|
if not text and allow_empty:
|
|
return True
|
|
if not text:
|
|
return f"{input_type.capitalize()} cannot be empty"
|
|
is_valid, error_msg = validator(text)
|
|
if is_valid:
|
|
return True
|
|
return error_msg
|
|
|
|
result = questionary.text(prompt, validate=q_validator).ask()
|
|
if result is None:
|
|
raise KeyboardInterrupt
|
|
return result
|
|
|
|
def get_app_names(self) -> list[str]:
|
|
user_input = questionary.text(
|
|
"Enter app names (comma-separated or single, e.g. users, products, orders):",
|
|
validate=lambda text: True if text.strip() else "At least one app name is required",
|
|
).ask()
|
|
|
|
if user_input is None:
|
|
raise KeyboardInterrupt
|
|
|
|
if "," in user_input:
|
|
return self._parse_comma_separated_apps(user_input)
|
|
|
|
return self._get_apps_starting_with(user_input.strip())
|
|
|
|
def _parse_comma_separated_apps(self, user_input: str) -> list[str]:
|
|
app_list = [app.strip() for app in user_input.split(",") if app.strip()]
|
|
|
|
if not app_list:
|
|
UIFormatter.print_error("At least one app name is required")
|
|
return self.get_app_names()
|
|
|
|
return self._validate_app_list(app_list)
|
|
|
|
def _get_apps_starting_with(self, first_app: str) -> list[str]:
|
|
return self._validate_app_list([first_app])
|
|
|
|
def _validate_app_list(self, app_list: list[str]) -> list[str]:
|
|
invalid_apps = []
|
|
for app in app_list:
|
|
is_valid, error_msg = validate_app_name(app)
|
|
if not is_valid:
|
|
invalid_apps.append((app, error_msg))
|
|
|
|
if invalid_apps:
|
|
for app, error_msg in invalid_apps:
|
|
UIFormatter.print_error(f"Invalid app name '{app}': {error_msg}")
|
|
return self.get_app_names()
|
|
|
|
return app_list
|
|
|
|
def _get_project_directory(self) -> str | None:
|
|
def validate_dir(text):
|
|
if not text or text == ".":
|
|
return True
|
|
|
|
is_valid, error_msg = validate_project_name(text)
|
|
if not is_valid:
|
|
return error_msg
|
|
|
|
if os.path.exists(text):
|
|
return f"Directory '{text}' already exists."
|
|
|
|
return True
|
|
|
|
project_dir = questionary.text("Enter project directory name:", validate=validate_dir).ask()
|
|
|
|
if project_dir is None:
|
|
raise KeyboardInterrupt
|
|
|
|
if not project_dir or project_dir == ".":
|
|
UIFormatter.print_info(f"Creating project in current directory: {os.getcwd()}")
|
|
return "."
|
|
|
|
return project_dir
|
|
|
|
def _get_selection(self, message: str, choices: list[tuple[str, str]], default: str = None) -> str:
|
|
q_choices = [questionary.Choice(title, value=value) for title, value in choices]
|
|
default_choice = next((c for c in q_choices if c.value == default), q_choices[0])
|
|
|
|
result = questionary.select(
|
|
message,
|
|
choices=q_choices,
|
|
default=default_choice,
|
|
).ask()
|
|
|
|
if result is None:
|
|
raise KeyboardInterrupt
|
|
|
|
return result
|
|
|
|
def get_cicd_choice(self) -> Tuple[bool, bool]:
|
|
choices = [
|
|
("None (skip CI/CD)", "none"),
|
|
("GitHub Actions only", "github"),
|
|
("GitLab CI only", "gitlab"),
|
|
("Both (GitHub Actions + GitLab CI)", "both"),
|
|
]
|
|
|
|
choice = self._get_selection("Select CI/CD pipeline:", choices, default="none")
|
|
|
|
choice_map = {
|
|
"both": (True, True),
|
|
"github": (True, False),
|
|
"gitlab": (False, True),
|
|
"none": (False, False),
|
|
}
|
|
return choice_map.get(choice, (False, False))
|
|
|
|
def get_nested_apps_config(self) -> Tuple[bool, str | None]:
|
|
choice = UIFormatter.confirm(
|
|
"Do you want to place apps inside a package directory (e.g. 'src/')?", default=False
|
|
)
|
|
|
|
if not choice:
|
|
return False, None
|
|
|
|
dir_name = self.get_validated_input(
|
|
"Enter directory name for apps package (e.g. src)",
|
|
validate_project_name,
|
|
"apps package name",
|
|
)
|
|
return True, dir_name
|
|
|
|
def get_database_config_choice(self) -> bool:
|
|
return UIFormatter.confirm("Use DATABASE_URL? (recommended for production)", default=True)
|
|
|
|
def get_database_type_choice(self) -> str:
|
|
choices = [
|
|
("PostgreSQL", "postgresql"),
|
|
("MySQL", "mysql"),
|
|
]
|
|
|
|
return self._get_selection("Choose database:", choices, default="postgresql")
|
|
|
|
def _get_structure_metadata(self, options: StructureOptions) -> Tuple[str, str, list[str], dict]:
|
|
"""Helper method to generate metadata dictionary."""
|
|
project_dir = options["project_dir"]
|
|
project_name = project_dir
|
|
app_names: list[str] = []
|
|
|
|
# Default package_name to "backend" if project_dir is "." or empty
|
|
package_name = get_package_name(project_dir)
|
|
|
|
project_module_name = "config" if options["predefined"] else "core" if options["unified"] else None
|
|
|
|
if options["single"]:
|
|
project_module_name = options["single_module_name"] or "project"
|
|
|
|
metadata = ProjectMetadata(
|
|
package_name=package_name,
|
|
use_github_actions=options["use_github"],
|
|
use_gitlab_ci=options["use_gitlab"],
|
|
nested_apps=not options["single"],
|
|
nested_dir="apps" if not options["single"] else None,
|
|
use_database_url=options["use_database_url"],
|
|
database_type=options["database_type"],
|
|
predefined_structure=options["predefined"],
|
|
unified_structure=options["unified"],
|
|
single_structure=options["single"],
|
|
project_module_name=project_module_name,
|
|
)
|
|
|
|
return project_name, "", app_names, metadata.to_dict()
|
|
|
|
|
|
def get_user_input() -> Tuple[str, str, str, list, dict]:
|
|
try:
|
|
collector = InputCollector()
|
|
|
|
structure_choices = [
|
|
("Standard structure (default Django layout)", "1"),
|
|
("Predefined structure (apps/users, apps/core, api/)", "2"),
|
|
("Unified structure (core/, apps/core, apps/api)", "3"),
|
|
("Single folder layout (everything in one folder)", "4"),
|
|
]
|
|
|
|
structure_choice = collector._get_selection("Choose structure type:", structure_choices, default="1")
|
|
|
|
use_standard = structure_choice == "1"
|
|
use_predefined = structure_choice == "2"
|
|
use_unified = structure_choice == "3"
|
|
use_single = structure_choice == "4"
|
|
|
|
# Step 1: Project Setup
|
|
project_dir = collector._get_project_directory()
|
|
if project_dir is None:
|
|
# Should be handled by questionary loop, but just in case
|
|
sys.exit(1)
|
|
|
|
project_name = project_dir
|
|
single_module_name = None
|
|
|
|
if use_standard:
|
|
console.print(f"[{UIColors.MUTED}]Common names: config, core, settings[/{UIColors.MUTED}]")
|
|
project_name = collector.get_validated_input(
|
|
"Enter Django project name", validate_project_name, "Django project name"
|
|
)
|
|
elif use_single:
|
|
console.print(f"[{UIColors.MUTED}]Common names: project, core, app[/{UIColors.MUTED}]")
|
|
single_module_name = (
|
|
collector.get_validated_input(
|
|
"Enter project configuration directory name (default: project)",
|
|
validate_project_name,
|
|
"directory name",
|
|
allow_empty=True,
|
|
)
|
|
or "project"
|
|
)
|
|
|
|
# Step 2: Database Configuration
|
|
database_type = collector.get_database_type_choice()
|
|
use_database_url = collector.get_database_config_choice()
|
|
|
|
# Step 3: Django Apps (Standard only)
|
|
nested = False
|
|
nested_dir = None
|
|
app_names = []
|
|
|
|
if use_standard:
|
|
nested, nested_dir = collector.get_nested_apps_config()
|
|
app_names = collector.get_app_names()
|
|
|
|
# Step 3/4: CI/CD Pipeline
|
|
use_github, use_gitlab = collector.get_cicd_choice()
|
|
|
|
# Generate Metadata
|
|
if use_standard:
|
|
package_name = get_package_name(project_dir)
|
|
metadata = ProjectMetadata(
|
|
package_name=package_name,
|
|
use_github_actions=use_github,
|
|
use_gitlab_ci=use_gitlab,
|
|
nested_apps=nested,
|
|
nested_dir=nested_dir,
|
|
use_database_url=use_database_url,
|
|
database_type=database_type,
|
|
)
|
|
return project_dir, project_name, app_names[0], app_names, metadata.to_dict()
|
|
else:
|
|
options = StructureOptions(
|
|
project_dir=project_dir,
|
|
predefined=use_predefined,
|
|
unified=use_unified,
|
|
single=use_single,
|
|
single_module_name=single_module_name,
|
|
database_type=database_type,
|
|
use_database_url=use_database_url,
|
|
use_github=use_github,
|
|
use_gitlab=use_gitlab,
|
|
)
|
|
project_name, primary_app, app_names, metadata_dict = collector._get_structure_metadata(options)
|
|
return project_dir, project_name, primary_app, app_names, metadata_dict
|
|
|
|
except KeyboardInterrupt:
|
|
UIFormatter.print_info("\nSetup cancelled by user.")
|
|
sys.exit(0)
|
|
|
|
|
|
def confirm_setup(project_dir: str, project_name: str, app_names: list, metadata: dict) -> bool:
|
|
console.print()
|
|
UIFormatter.print_separator()
|
|
console.print()
|
|
console.print(f"[{UIColors.INFO}]Setup Summary[/{UIColors.INFO}]")
|
|
console.print()
|
|
|
|
console.print(f"[{UIColors.HIGHLIGHT}]Project Directory:[/{UIColors.HIGHLIGHT}] {project_dir}")
|
|
console.print(f"[{UIColors.HIGHLIGHT}]Django Project:[/{UIColors.HIGHLIGHT}] {project_name}")
|
|
console.print(f"[{UIColors.HIGHLIGHT}]Apps:[/{UIColors.HIGHLIGHT}] {', '.join(app_names)}")
|
|
console.print(f"[{UIColors.HIGHLIGHT}]Package:[/{UIColors.HIGHLIGHT}] {metadata['package_name']}")
|
|
|
|
cicd_choices = _get_cicd_display(metadata)
|
|
console.print(f"[{UIColors.HIGHLIGHT}]CI/CD:[/{UIColors.HIGHLIGHT}] {cicd_choices}")
|
|
|
|
db_config = "DATABASE_URL" if metadata.get("use_database_url", True) else "Individual parameters"
|
|
console.print(f"[{UIColors.HIGHLIGHT}]Database Config:[/{UIColors.HIGHLIGHT}] {db_config}")
|
|
|
|
db_type = metadata.get("database_type", "postgresql").capitalize()
|
|
console.print(f"[{UIColors.HIGHLIGHT}]Database Type:[/{UIColors.HIGHLIGHT}] {db_type}")
|
|
|
|
console.print()
|
|
UIFormatter.print_separator()
|
|
console.print()
|
|
|
|
try:
|
|
return UIFormatter.confirm("Proceed with setup?", default=True)
|
|
|
|
except KeyboardInterrupt:
|
|
UIFormatter.print_info("\nSetup cancelled by user.")
|
|
return False
|
|
|
|
|
|
def _get_cicd_display(metadata: dict) -> str:
|
|
choices = []
|
|
if metadata.get("use_github_actions", False):
|
|
choices.append("GitHub Actions")
|
|
if metadata.get("use_gitlab_ci", False):
|
|
choices.append("GitLab CI")
|
|
return ", ".join(choices) if choices else "None"
|