feat: add single folder structure option for project generation
This commit is contained in:
4 files changed
+117
-7
No files matched your search
@@ -54,6 +54,15 @@ class Cli:
|
||||
# propagate to managers
|
||||
self.project_manager.app_names = self.app_names
|
||||
|
||||
# If single structure is enabled, adjust metadata defaults
|
||||
if self.metadata.get("single_structure"):
|
||||
# Module name is already set in input_handler
|
||||
# No nested apps
|
||||
self.metadata.setdefault("nested_apps", False)
|
||||
if not self.app_names:
|
||||
self.app_names = []
|
||||
self.project_manager.app_names = self.app_names
|
||||
|
||||
steps: List[Tuple[str, Callable[[], bool]]] = []
|
||||
|
||||
steps.append(("Creating Django project", self.project_manager.create_project))
|
||||
@@ -61,6 +70,9 @@ class Cli:
|
||||
if self.metadata.get("unified_structure"):
|
||||
# Build unified structure
|
||||
steps.append(("Creating unified structure", self.file_manager.create_unified_structure))
|
||||
elif self.metadata.get("single_structure"):
|
||||
# Build single folder structure
|
||||
steps.append(("Creating single folder structure", self.file_manager.create_single_structure))
|
||||
elif self.metadata.get("predefined_structure"):
|
||||
# Build custom tree and then inject apps into settings
|
||||
steps.append(("Creating predefined structure", self.file_manager.create_predefined_structure))
|
||||
|
||||
@@ -303,6 +303,81 @@ class FileManager:
|
||||
|
||||
return True
|
||||
|
||||
def create_single_structure(self) -> bool:
|
||||
# Create project directory structure (project/)
|
||||
project_dir = os.path.join(self.project_root, self.module_name)
|
||||
create_directory_with_init(project_dir, f"Created {self.module_name}/__init__.py")
|
||||
|
||||
# Create project/settings directory
|
||||
settings_dir = os.path.join(project_dir, "settings")
|
||||
create_directory_with_init(settings_dir, f"Created {self.module_name}/settings/__init__.py")
|
||||
|
||||
# Generate secret key for development settings
|
||||
from djinit.scripts.secretkey_generator import generate_secret_key
|
||||
|
||||
secret_key = generate_secret_key()
|
||||
|
||||
# Create settings files
|
||||
base_context = {
|
||||
"project_name": self.module_name,
|
||||
"app_names": [self.module_name], # Project acts as the app
|
||||
"use_database_url": self.metadata.get("use_database_url", True),
|
||||
}
|
||||
dev_context = {"secret_key": secret_key}
|
||||
|
||||
for filename, context in [
|
||||
("base.py", base_context),
|
||||
("development.py", dev_context),
|
||||
("production.py", base_context),
|
||||
]:
|
||||
filepath = os.path.join(settings_dir, filename)
|
||||
template_path = f"project/settings/{filename.replace('.py', '')}.j2"
|
||||
create_file_from_template(
|
||||
filepath, template_path, context, f"Created {self.module_name}/settings/{filename}", should_format=True
|
||||
)
|
||||
|
||||
# Core project files
|
||||
project_files = [
|
||||
("urls.py", "project/urls_with_api.j2", {"api_module": f"{self.module_name}.api"}),
|
||||
("wsgi.py", "project/wsgi.j2", {}),
|
||||
("asgi.py", "project/asgi.j2", {}),
|
||||
]
|
||||
for filename, template, context in project_files:
|
||||
filepath = os.path.join(project_dir, filename)
|
||||
create_file_from_template(filepath, template, context, f"Created {self.module_name}/{filename}", should_format=True)
|
||||
|
||||
# Create app component directories
|
||||
components = ["admin", "api", "models", "routes", "serializers", "tests", "views"]
|
||||
self._create_subdirectories_with_init(project_dir, components, f"{self.module_name}/")
|
||||
|
||||
# Create basic files for components to ensure valid python packages/modules
|
||||
|
||||
# admin
|
||||
create_file_from_template(
|
||||
os.path.join(project_dir, "admin", "__init__.py"),
|
||||
"base/admin.j2",
|
||||
{},
|
||||
f"Created {self.module_name}/admin/__init__.py"
|
||||
)
|
||||
|
||||
# api
|
||||
create_file_from_template(
|
||||
os.path.join(project_dir, "api", "urls.py"),
|
||||
"predefined/api/urls.j2",
|
||||
{},
|
||||
f"Created {self.module_name}/api/urls.py"
|
||||
)
|
||||
|
||||
# models
|
||||
create_file_from_template(
|
||||
os.path.join(project_dir, "models", "__init__.py"),
|
||||
"base/models.j2",
|
||||
{},
|
||||
f"Created {self.module_name}/models/__init__.py"
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
def create_procfile(self) -> bool:
|
||||
context = {"project_name": self.project_name}
|
||||
return self._render_and_create_file(
|
||||
|
||||
@@ -37,7 +37,9 @@ class ProjectMetadata:
|
||||
use_database_url: bool = False
|
||||
# predefined structure support
|
||||
predefined_structure: bool = False
|
||||
predefined_structure: bool = False
|
||||
unified_structure: bool = False
|
||||
single_structure: bool = False
|
||||
project_module_name: str | None = None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
@@ -49,7 +51,9 @@ class ProjectMetadata:
|
||||
"nested_dir": self.nested_dir,
|
||||
"use_database_url": self.use_database_url,
|
||||
"predefined_structure": self.predefined_structure,
|
||||
"predefined_structure": self.predefined_structure,
|
||||
"unified_structure": self.unified_structure,
|
||||
"single_structure": self.single_structure,
|
||||
"project_module_name": self.project_module_name,
|
||||
}
|
||||
|
||||
@@ -250,7 +254,7 @@ class InputCollector:
|
||||
return CharReader.get_yes_no(f"[{UIColors.HIGHLIGHT}]Use DATABASE_URL? (Y/n):[/{UIColors.HIGHLIGHT}]") == "y"
|
||||
|
||||
def _get_structure_metadata(
|
||||
self, project_dir: str, predefined: bool = False, unified: bool = False
|
||||
self, project_dir: str, predefined: bool = False, unified: bool = False, single: bool = False, single_module_name: str = None
|
||||
) -> Tuple[str, str, list[str], dict]:
|
||||
"""Helper method to get metadata for predefined/unified structures."""
|
||||
project_name = project_dir
|
||||
@@ -266,6 +270,9 @@ class InputCollector:
|
||||
package_name = get_package_name(project_dir)
|
||||
|
||||
project_module_name = "config" if predefined else "core" if unified else None
|
||||
|
||||
if single:
|
||||
project_module_name = single_module_name or "project"
|
||||
|
||||
metadata = ProjectMetadata(
|
||||
package_name=package_name,
|
||||
@@ -276,6 +283,7 @@ class InputCollector:
|
||||
use_database_url=use_database_url,
|
||||
predefined_structure=predefined,
|
||||
unified_structure=unified,
|
||||
single_structure=single,
|
||||
project_module_name=project_module_name,
|
||||
)
|
||||
|
||||
@@ -389,10 +397,10 @@ class CharReader:
|
||||
def get_structure_choice() -> str:
|
||||
"""Get structure type choice without requiring Enter key."""
|
||||
return CharReader._get_validated_char_input(
|
||||
prompt=f"[{UIColors.HIGHLIGHT}]Choose structure type (1/2/3) [default: 1]:[/{UIColors.HIGHLIGHT}]",
|
||||
valid_keys=["1", "2", "3"],
|
||||
prompt=f"[{UIColors.HIGHLIGHT}]Choose structure type (1/2/3/4) [default: 1]:[/{UIColors.HIGHLIGHT}]",
|
||||
valid_keys=["1", "2", "3", "4"],
|
||||
default="1",
|
||||
error_message="Invalid input '{response}'. Please enter '1', '2', or '3'.",
|
||||
error_message="Invalid input '{response}'. Please enter '1', '2', '3', or '4'.",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@@ -459,14 +467,16 @@ def get_user_input() -> Tuple[str, str, str, list, dict]:
|
||||
console.print(f"[{UIColors.MUTED}]1. Standard structure (default Django layout)[/{UIColors.MUTED}]")
|
||||
console.print(f"[{UIColors.MUTED}]2. Predefined structure (apps/users, apps/core, api/)[/{UIColors.MUTED}]")
|
||||
console.print(f"[{UIColors.MUTED}]3. Unified structure (core/, apps/core, apps/api)[/{UIColors.MUTED}]")
|
||||
console.print(f"[{UIColors.MUTED}]4. Single folder layout (everything in one folder)[/{UIColors.MUTED}]")
|
||||
console.print()
|
||||
|
||||
structure_choice = CharReader.get_structure_choice()
|
||||
|
||||
use_predefined = structure_choice == "2"
|
||||
use_unified = structure_choice == "3"
|
||||
use_single = structure_choice == "4"
|
||||
|
||||
if use_predefined or use_unified:
|
||||
if use_predefined or use_unified or use_single:
|
||||
UIFormatter.print_separator()
|
||||
console.print(f"\n[{UIColors.INFO}]Step 1: Project Setup[/{UIColors.INFO}]\n")
|
||||
console.print(
|
||||
@@ -476,9 +486,22 @@ def get_user_input() -> Tuple[str, str, str, list, dict]:
|
||||
if project_dir is None:
|
||||
UIFormatter.print_error("Maximum attempts reached for project directory name. Exiting.")
|
||||
sys.exit(1)
|
||||
|
||||
# Ask for module name after project directory for single layout
|
||||
single_module_name = None
|
||||
if use_single:
|
||||
console.print()
|
||||
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"
|
||||
console.print()
|
||||
|
||||
project_name, primary_app, app_names, metadata_dict = collector._get_structure_metadata(
|
||||
project_dir, predefined=use_predefined, unified=use_unified
|
||||
project_dir, predefined=use_predefined, unified=use_unified, single=use_single, single_module_name=single_module_name
|
||||
)
|
||||
|
||||
return project_dir, project_name, primary_app, app_names, metadata_dict
|
||||
|
||||
@@ -114,7 +114,7 @@ class ProjectManager:
|
||||
apps_base_dir = self._get_apps_base_dir()
|
||||
|
||||
# In predefined or unified structure, apps layout differs; skip strict per-app validation
|
||||
if not self.metadata.get("predefined_structure") and not self.metadata.get("unified_structure"):
|
||||
if not self.metadata.get("predefined_structure") and not self.metadata.get("unified_structure") and not self.metadata.get("single_structure"):
|
||||
for app_name in self.app_names:
|
||||
app_files = [
|
||||
os.path.join(apps_base_dir, app_name, "__init__.py"),
|
||||
|
||||
Reference in new issue
Block a user