feat: add single folder structure option for project generation

This commit is contained in:
sankalp committed 2025-11-25 17:46:21 +05:45
1 parent 1a5b0c6f27
commit 42cab7ef44
4 files changed
+117 -7

No files matched your search

+12
View File
@@ -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))
+75
View File
@@ -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(
+29 -6
View 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
+1 -1
View File
@@ -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"),