refactor(structure): streamline project structure creation and file generation
This commit is contained in:
32 files changed
+334
-592
No files matched your search
@@ -19,12 +19,7 @@ class Cli:
|
||||
self.primary_app = primary_app
|
||||
self.app_names = app_names
|
||||
self.metadata = metadata
|
||||
<<<<<<< HEAD
|
||||
if project_dir == ".": # handle '.' for current directory
|
||||
=======
|
||||
# Handle '.' for current directory
|
||||
if project_dir == ".":
|
||||
>>>>>>> origin/main
|
||||
self.project_root = os.getcwd()
|
||||
else:
|
||||
self.project_root = os.path.join(os.getcwd(), project_dir)
|
||||
|
||||
@@ -4,19 +4,14 @@ Handles creation of Django apps and updating settings.
|
||||
"""
|
||||
|
||||
import os
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
import subprocess
|
||||
import sys
|
||||
>>>>>>> origin/main
|
||||
from typing import Optional
|
||||
|
||||
from djinit.scripts.console_ui import UIFormatter
|
||||
from djinit.scripts.django_helper import DjangoHelper
|
||||
from djinit.scripts.template_engine import template_engine
|
||||
from djinit.utils import (
|
||||
calculate_app_module_path,
|
||||
create_file_with_content,
|
||||
create_directory_with_init,
|
||||
create_file_from_template,
|
||||
create_init_file,
|
||||
detect_nested_structure_from_settings,
|
||||
extract_existing_apps,
|
||||
@@ -32,6 +27,7 @@ class AppManager:
|
||||
self.app_name = app_name
|
||||
self.current_dir = os.getcwd()
|
||||
self.manage_py_path = os.path.join(self.current_dir, "manage.py")
|
||||
self._project_structure_cache = None
|
||||
|
||||
def create_app(self) -> bool:
|
||||
if not self._is_django_project():
|
||||
@@ -61,34 +57,22 @@ class AppManager:
|
||||
if self._is_predefined_structure():
|
||||
app_path = os.path.join(self.current_dir, "apps", self.app_name)
|
||||
else:
|
||||
is_nested, nested_dir, apps_base_dir = self._detect_project_structure()
|
||||
_, _, apps_base_dir = self._get_project_structure()
|
||||
app_path = os.path.join(apps_base_dir, self.app_name)
|
||||
return os.path.exists(app_path)
|
||||
|
||||
def _create_django_app(self) -> bool:
|
||||
# Find where to create the app (flat or nested structure)
|
||||
is_nested, nested_dir, apps_base_dir = self._detect_project_structure()
|
||||
|
||||
# Verify manage.py exists in project root
|
||||
manage_py_path = os.path.join(self.current_dir, "manage.py")
|
||||
if not os.path.exists(manage_py_path):
|
||||
if not os.path.exists(self.manage_py_path):
|
||||
UIFormatter.print_error("Could not find manage.py file in project root")
|
||||
return False
|
||||
|
||||
<<<<<<< HEAD
|
||||
# If predefined structure detected (apps/ exists), scaffold nested layout using custom templates
|
||||
if self._is_predefined_structure():
|
||||
return self._create_predefined_app(os.path.join(self.current_dir, "apps"))
|
||||
=======
|
||||
# Create app in the correct location
|
||||
# Use 'python -m django' instead of 'django-admin' for better compatibility
|
||||
with change_cwd(apps_base_dir):
|
||||
subprocess.run(
|
||||
[sys.executable, "-m", "django", "startapp", self.app_name], capture_output=True, text=True, check=True
|
||||
)
|
||||
>>>>>>> origin/main
|
||||
|
||||
# Otherwise, use standard flat app generation
|
||||
_, _, apps_base_dir = self._get_project_structure()
|
||||
success = DjangoHelper.startapp(self.app_name, apps_base_dir)
|
||||
if success:
|
||||
UIFormatter.print_success(f"Created Django app '{self.app_name}' in {apps_base_dir}")
|
||||
@@ -115,7 +99,7 @@ class AppManager:
|
||||
if self._is_predefined_structure():
|
||||
app_module_path = f"apps.{self.app_name}"
|
||||
else:
|
||||
is_nested, nested_dir, apps_base_dir = self._detect_project_structure()
|
||||
is_nested, nested_dir, _ = self._get_project_structure()
|
||||
app_module_path = calculate_app_module_path(self.app_name, is_nested, nested_dir)
|
||||
|
||||
# Check if app is already in USER_DEFINED_APPS
|
||||
@@ -139,15 +123,20 @@ class AppManager:
|
||||
UIFormatter.print_success(f"Added '{app_module_path}' to USER_DEFINED_APPS in base.py")
|
||||
return True
|
||||
|
||||
def _detect_project_structure(self) -> tuple[bool, Optional[str], str]:
|
||||
# Find project directory with settings/base.py
|
||||
project_dir, settings_base_path = find_project_dir(self.current_dir)
|
||||
def _get_project_structure(self) -> tuple[bool, Optional[str], str]:
|
||||
"""Get project structure with caching to avoid repeated detection."""
|
||||
if self._project_structure_cache is None:
|
||||
# Find project directory with settings/base.py
|
||||
project_dir, settings_base_path = find_project_dir(self.current_dir)
|
||||
|
||||
if project_dir is None:
|
||||
return False, None, self.current_dir
|
||||
|
||||
# Detect nested structure from settings file
|
||||
return detect_nested_structure_from_settings(settings_base_path, self.current_dir)
|
||||
if project_dir is None:
|
||||
self._project_structure_cache = (False, None, self.current_dir)
|
||||
else:
|
||||
# Detect nested structure from settings file
|
||||
self._project_structure_cache = detect_nested_structure_from_settings(
|
||||
settings_base_path, self.current_dir
|
||||
)
|
||||
return self._project_structure_cache
|
||||
|
||||
def _is_predefined_structure(self) -> bool:
|
||||
# Heuristic: presence of 'apps' directory at project root (where manage.py lives) and 'api' dir
|
||||
@@ -161,15 +150,18 @@ class AppManager:
|
||||
os.makedirs(app_dir, exist_ok=True)
|
||||
create_init_file(app_dir, f"Created apps/{self.app_name}/__init__.py")
|
||||
|
||||
# apps.py using standard template
|
||||
apps_py_content = template_engine.render_template("base/apps.j2", {"app_name": self.app_name})
|
||||
create_file_with_content(
|
||||
os.path.join(app_dir, "apps.py"), apps_py_content, f"Created apps/{self.app_name}/apps.py"
|
||||
)
|
||||
|
||||
# Compute names for generic templates
|
||||
model_class_name = "".join([part.capitalize() for part in self.app_name.split("_")])
|
||||
app_module = f"apps.{self.app_name}"
|
||||
base_context = {"app_name": self.app_name, "app_module": app_module, "model_class_name": model_class_name}
|
||||
|
||||
# Create apps.py
|
||||
create_file_from_template(
|
||||
os.path.join(app_dir, "apps.py"),
|
||||
"base/apps.j2",
|
||||
{"app_name": self.app_name},
|
||||
f"Created apps/{self.app_name}/apps.py",
|
||||
)
|
||||
|
||||
# Subfolders
|
||||
subfolders = {
|
||||
@@ -181,25 +173,19 @@ class AppManager:
|
||||
}
|
||||
for folder, files in subfolders.items():
|
||||
folder_path = os.path.join(app_dir, folder)
|
||||
os.makedirs(folder_path, exist_ok=True)
|
||||
create_init_file(folder_path, f"Created apps/{self.app_name}/{folder}/__init__.py")
|
||||
for filename, tpl in files:
|
||||
content = template_engine.render_template(
|
||||
tpl,
|
||||
{"app_name": self.app_name, "app_module": app_module, "model_class_name": model_class_name},
|
||||
)
|
||||
create_file_with_content(
|
||||
os.path.join(folder_path, filename),
|
||||
content,
|
||||
f"Created apps/{self.app_name}/{folder}/{filename}",
|
||||
create_directory_with_init(folder_path, f"Created apps/{self.app_name}/{folder}/__init__.py")
|
||||
for filename, template in files:
|
||||
file_path = os.path.join(folder_path, filename)
|
||||
create_file_from_template(
|
||||
file_path, template, base_context, f"Created apps/{self.app_name}/{folder}/{filename}"
|
||||
)
|
||||
|
||||
# urls.py at app root
|
||||
urls_content = template_engine.render_template(
|
||||
"predefined/apps/generic/urls.j2", {"app_name": self.app_name, "app_module": app_module}
|
||||
)
|
||||
create_file_with_content(
|
||||
os.path.join(app_dir, "urls.py"), urls_content, f"Created apps/{self.app_name}/urls.py"
|
||||
create_file_from_template(
|
||||
os.path.join(app_dir, "urls.py"),
|
||||
"predefined/apps/generic/urls.j2",
|
||||
{"app_name": self.app_name, "app_module": app_module},
|
||||
f"Created apps/{self.app_name}/urls.py",
|
||||
)
|
||||
|
||||
# Add route include to api/v1/urls.py if present
|
||||
|
||||
@@ -7,7 +7,12 @@ This module provides functions to create Django projects and apps without requir
|
||||
import os
|
||||
|
||||
from djinit.scripts.console_ui import UIFormatter
|
||||
from djinit.utils import create_file_from_template, create_init_file
|
||||
from djinit.utils import (
|
||||
create_directory_with_init,
|
||||
create_file_from_template,
|
||||
create_files_from_templates,
|
||||
create_init_file,
|
||||
)
|
||||
|
||||
|
||||
class DjangoHelper:
|
||||
@@ -32,54 +37,28 @@ class DjangoHelper:
|
||||
|
||||
# Create project config directory
|
||||
project_config_dir = os.path.join(directory, project_name)
|
||||
os.makedirs(project_config_dir, exist_ok=True)
|
||||
create_init_file(project_config_dir, f"Created {project_name}/__init__.py")
|
||||
create_directory_with_init(project_config_dir, f"Created {project_name}/__init__.py")
|
||||
|
||||
# Create settings directory and files
|
||||
settings_dir = os.path.join(project_config_dir, "settings")
|
||||
os.makedirs(settings_dir, exist_ok=True)
|
||||
create_init_file(settings_dir, f"Created {project_name}/settings/__init__.py")
|
||||
create_directory_with_init(settings_dir, f"Created {project_name}/settings/__init__.py")
|
||||
|
||||
base_context = {"project_name": project_name, "app_names": []}
|
||||
create_file_from_template(
|
||||
os.path.join(settings_dir, "base.py"),
|
||||
"project/settings/base.j2",
|
||||
base_context,
|
||||
f"Created {project_name}/settings/base.py",
|
||||
)
|
||||
create_file_from_template(
|
||||
os.path.join(settings_dir, "development.py"),
|
||||
"project/settings/development.j2",
|
||||
{},
|
||||
f"Created {project_name}/settings/development.py",
|
||||
)
|
||||
create_file_from_template(
|
||||
os.path.join(settings_dir, "production.py"),
|
||||
"project/settings/production.j2",
|
||||
{},
|
||||
f"Created {project_name}/settings/production.py",
|
||||
)
|
||||
settings_files = [
|
||||
("base.py", "project/settings/base.j2", base_context),
|
||||
("development.py", "project/settings/development.j2", {}),
|
||||
("production.py", "project/settings/production.j2", {}),
|
||||
]
|
||||
create_files_from_templates(settings_dir, settings_files, f"{project_name}/settings/")
|
||||
|
||||
# Create project-level files
|
||||
urls_context = {"project_name": project_name, "django_version": DjangoHelper.DJANGO_VERSION}
|
||||
create_file_from_template(
|
||||
os.path.join(project_config_dir, "urls.py"),
|
||||
"project/urls.j2",
|
||||
urls_context,
|
||||
f"Created {project_name}/urls.py",
|
||||
)
|
||||
create_file_from_template(
|
||||
os.path.join(project_config_dir, "wsgi.py"),
|
||||
"project/wsgi.j2",
|
||||
{},
|
||||
f"Created {project_name}/wsgi.py",
|
||||
)
|
||||
create_file_from_template(
|
||||
os.path.join(project_config_dir, "asgi.py"),
|
||||
"project/asgi.j2",
|
||||
{},
|
||||
f"Created {project_name}/asgi.py",
|
||||
)
|
||||
project_files = [
|
||||
("urls.py", "project/urls.j2", urls_context),
|
||||
("wsgi.py", "project/wsgi.j2", {}),
|
||||
("asgi.py", "project/asgi.j2", {}),
|
||||
]
|
||||
create_files_from_templates(project_config_dir, project_files, f"{project_name}/")
|
||||
|
||||
return True
|
||||
|
||||
@@ -99,28 +78,21 @@ class DjangoHelper:
|
||||
create_init_file(app_dir, f"Created {app_name}/__init__.py")
|
||||
|
||||
app_files = [
|
||||
("apps.py", "base/apps.j2"),
|
||||
("models.py", "base/models.j2"),
|
||||
("views.py", "base/views.j2"),
|
||||
("admin.py", "base/admin.j2"),
|
||||
("urls.py", "base/urls.j2"),
|
||||
("serializers.py", "base/serializers.j2"),
|
||||
("routes.py", "base/routes.j2"),
|
||||
("tests.py", "base/tests.j2"),
|
||||
("apps.py", "base/apps.j2", context),
|
||||
("models.py", "base/models.j2", context),
|
||||
("views.py", "base/views.j2", context),
|
||||
("admin.py", "base/admin.j2", context),
|
||||
("urls.py", "base/urls.j2", context),
|
||||
("serializers.py", "base/serializers.j2", context),
|
||||
("routes.py", "base/routes.j2", context),
|
||||
("tests.py", "base/tests.j2", context),
|
||||
]
|
||||
|
||||
for filename, template_path in app_files:
|
||||
create_file_from_template(
|
||||
os.path.join(app_dir, filename),
|
||||
template_path,
|
||||
context,
|
||||
f"Created {app_name}/{filename}",
|
||||
)
|
||||
create_files_from_templates(app_dir, app_files, f"{app_name}/")
|
||||
|
||||
# Create migrations directory and __init__.py
|
||||
migrations_dir = os.path.join(app_dir, "migrations")
|
||||
os.makedirs(migrations_dir, exist_ok=True)
|
||||
create_init_file(migrations_dir, f"Created {app_name}/migrations/__init__.py")
|
||||
create_directory_with_init(migrations_dir, f"Created {app_name}/migrations/__init__.py")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
+65
-66
@@ -9,8 +9,11 @@ from djinit.scripts.template_engine import template_engine
|
||||
from djinit.utils import (
|
||||
calculate_app_module_paths,
|
||||
change_cwd,
|
||||
create_directory_with_init,
|
||||
create_file_from_template,
|
||||
create_file_with_content,
|
||||
create_init_file,
|
||||
create_files_from_templates,
|
||||
get_package_name,
|
||||
)
|
||||
|
||||
|
||||
@@ -42,21 +45,29 @@ class FileManager:
|
||||
content = template_engine.render_template(template_path, context)
|
||||
return create_file_with_content(filepath, content, message, should_format=should_format)
|
||||
|
||||
def _ensure_directory_with_init(self, dir_path: str, message: str) -> None:
|
||||
"""Helper to create directory and __init__.py file."""
|
||||
os.makedirs(dir_path, exist_ok=True)
|
||||
create_init_file(dir_path, message)
|
||||
|
||||
def _create_files_from_specs(self, base_dir: str, folder_specs: dict, base_context: dict = None) -> None:
|
||||
"""Helper to create multiple folders and files from a specification dict."""
|
||||
base_context = base_context or {}
|
||||
for folder, files in folder_specs.items():
|
||||
folder_path = os.path.join(base_dir, folder)
|
||||
self._ensure_directory_with_init(folder_path, f"Created {folder_path}/__init__.py")
|
||||
create_directory_with_init(folder_path, f"Created {folder_path}/__init__.py")
|
||||
for filename, template_path in files:
|
||||
file_path = os.path.join(folder_path, filename)
|
||||
content = template_engine.render_template(template_path, base_context)
|
||||
create_file_with_content(file_path, content, f"Created {file_path}")
|
||||
create_file_from_template(file_path, template_path, base_context, f"Created {file_path}")
|
||||
|
||||
def _create_apps_py(self, app_dir: str, app_name: str, message: str = None, should_format: bool = False) -> None:
|
||||
"""Helper to create apps.py file for an app."""
|
||||
apps_py_path = os.path.join(app_dir, "apps.py")
|
||||
context = {"app_name": app_name} if app_name else {}
|
||||
msg = message or f"Created {os.path.basename(app_dir)}/apps.py"
|
||||
create_file_from_template(apps_py_path, "base/apps.j2", context, msg, should_format=should_format)
|
||||
|
||||
def _create_subdirectories_with_init(self, base_dir: str, subdirs: list, prefix: str = "") -> None:
|
||||
"""Helper to create multiple subdirectories with __init__.py files."""
|
||||
for subdir in subdirs:
|
||||
subdir_path = os.path.join(base_dir, subdir)
|
||||
message = f"Created {prefix}{subdir}/__init__.py" if prefix else f"Created {subdir}/__init__.py"
|
||||
create_directory_with_init(subdir_path, message)
|
||||
|
||||
def create_gitignore(self) -> bool:
|
||||
return self._render_and_create_file(".gitignore", "shared/gitignore.j2", {}, "Created .gitignore file")
|
||||
@@ -96,8 +107,7 @@ class FileManager:
|
||||
def create_pyproject_toml(self, metadata: dict) -> bool:
|
||||
# Default package_name to "backend" if it's "." or empty
|
||||
package_name = metadata.get("package_name", "backend")
|
||||
if package_name == "." or not package_name:
|
||||
package_name = "backend"
|
||||
package_name = get_package_name(package_name)
|
||||
context = {"package_name": package_name, "project_name": self.project_name}
|
||||
return self._render_and_create_file(
|
||||
"pyproject.toml",
|
||||
@@ -134,16 +144,12 @@ class FileManager:
|
||||
def create_predefined_structure(self) -> bool:
|
||||
# Ensure apps package
|
||||
apps_dir = os.path.join(self.project_root, "apps")
|
||||
self._ensure_directory_with_init(apps_dir, "Created apps/__init__.py")
|
||||
create_directory_with_init(apps_dir, "Created apps/__init__.py")
|
||||
|
||||
# users app
|
||||
users_dir = os.path.join(apps_dir, "users")
|
||||
self._ensure_directory_with_init(users_dir, "Created apps/users/__init__.py")
|
||||
|
||||
# apps.py can reuse standard app template
|
||||
users_apps_path = os.path.join(users_dir, "apps.py")
|
||||
content = template_engine.render_template("base/apps.j2", {"app_name": "users"})
|
||||
create_file_with_content(users_apps_path, content, "Created apps/users/apps.py")
|
||||
create_directory_with_init(users_dir, "Created apps/users/__init__.py")
|
||||
self._create_apps_py(users_dir, "users")
|
||||
|
||||
# Nested folders and files for users
|
||||
users_subfolders = {
|
||||
@@ -157,12 +163,13 @@ class FileManager:
|
||||
|
||||
# users urls.py at app root
|
||||
users_urls_path = os.path.join(users_dir, "urls.py")
|
||||
content = template_engine.render_template("predefined/apps/users/urls.j2", {"app_module": "apps.users"})
|
||||
create_file_with_content(users_urls_path, content, "Created apps/users/urls.py")
|
||||
create_file_from_template(
|
||||
users_urls_path, "predefined/apps/users/urls.j2", {"app_module": "apps.users"}, "Created apps/users/urls.py"
|
||||
)
|
||||
|
||||
# core app
|
||||
core_dir = os.path.join(apps_dir, "core")
|
||||
self._ensure_directory_with_init(core_dir, "Created apps/core/__init__.py")
|
||||
create_directory_with_init(core_dir, "Created apps/core/__init__.py")
|
||||
|
||||
# core subfolders and files
|
||||
core_subfolders = {
|
||||
@@ -174,28 +181,30 @@ class FileManager:
|
||||
|
||||
# core/exceptions.py
|
||||
exceptions_path = os.path.join(core_dir, "exceptions.py")
|
||||
content = template_engine.render_template("predefined/core/exceptions.j2", {})
|
||||
create_file_with_content(exceptions_path, content, "Created apps/core/exceptions.py")
|
||||
create_file_from_template(
|
||||
exceptions_path, "predefined/core/exceptions.j2", {}, "Created apps/core/exceptions.py"
|
||||
)
|
||||
|
||||
# api package
|
||||
api_dir = os.path.join(self.project_root, "api")
|
||||
self._ensure_directory_with_init(api_dir, "Created api/__init__.py")
|
||||
create_directory_with_init(api_dir, "Created api/__init__.py")
|
||||
api_urls_path = os.path.join(api_dir, "urls.py")
|
||||
content = template_engine.render_template("predefined/api/urls.j2", {})
|
||||
create_file_with_content(api_urls_path, content, "Created api/urls.py")
|
||||
create_file_from_template(api_urls_path, "predefined/api/urls.j2", {}, "Created api/urls.py")
|
||||
|
||||
# api/v1
|
||||
api_v1_dir = os.path.join(api_dir, "v1")
|
||||
self._ensure_directory_with_init(api_v1_dir, "Created api/v1/__init__.py")
|
||||
create_directory_with_init(api_v1_dir, "Created api/v1/__init__.py")
|
||||
api_v1_urls_path = os.path.join(api_v1_dir, "urls.py")
|
||||
content = template_engine.render_template("predefined/api/v1/urls.j2", {})
|
||||
create_file_with_content(api_v1_urls_path, content, "Created api/v1/urls.py")
|
||||
create_file_from_template(api_v1_urls_path, "predefined/api/v1/urls.j2", {}, "Created api/v1/urls.py")
|
||||
|
||||
# overwrite project urls to include api
|
||||
project_urls_path = os.path.join(self.project_configs, "urls.py")
|
||||
content = template_engine.render_template("project/urls_with_api.j2", {"api_module": "api"})
|
||||
create_file_with_content(
|
||||
project_urls_path, content, "Updated project urls.py to include API routes", should_format=True
|
||||
create_file_from_template(
|
||||
project_urls_path,
|
||||
"project/urls_with_api.j2",
|
||||
{"api_module": "api"},
|
||||
"Updated project urls.py to include API routes",
|
||||
should_format=True,
|
||||
)
|
||||
|
||||
return True
|
||||
@@ -203,11 +212,11 @@ class FileManager:
|
||||
def create_unified_structure(self) -> bool:
|
||||
# Create core directory structure
|
||||
core_dir = os.path.join(self.project_root, "core")
|
||||
self._ensure_directory_with_init(core_dir, "Created core/__init__.py")
|
||||
create_directory_with_init(core_dir, "Created core/__init__.py")
|
||||
|
||||
# Create core/settings directory
|
||||
settings_dir = os.path.join(core_dir, "settings")
|
||||
self._ensure_directory_with_init(settings_dir, "Created core/settings/__init__.py")
|
||||
create_directory_with_init(settings_dir, "Created core/settings/__init__.py")
|
||||
|
||||
# Generate secret key for development settings
|
||||
from djinit.scripts.secretkey_generator import generate_secret_key
|
||||
@@ -230,8 +239,10 @@ class FileManager:
|
||||
("production.py", base_context),
|
||||
]:
|
||||
filepath = os.path.join(settings_dir, filename)
|
||||
content = template_engine.render_template(f"project/settings/{filename.replace('.py', '')}.j2", context)
|
||||
create_file_with_content(filepath, content, f"Created core/settings/{filename}", should_format=True)
|
||||
template_path = f"project/settings/{filename.replace('.py', '')}.j2"
|
||||
create_file_from_template(
|
||||
filepath, template_path, context, f"Created core/settings/{filename}", should_format=True
|
||||
)
|
||||
|
||||
# Core project files
|
||||
core_files = [
|
||||
@@ -241,66 +252,54 @@ class FileManager:
|
||||
]
|
||||
for filename, template, context in core_files:
|
||||
filepath = os.path.join(core_dir, filename)
|
||||
content = template_engine.render_template(template, context)
|
||||
create_file_with_content(filepath, content, f"Created core/{filename}", should_format=True)
|
||||
create_file_from_template(filepath, template, context, f"Created core/{filename}", should_format=True)
|
||||
|
||||
# Create apps directory
|
||||
apps_dir = os.path.join(self.project_root, "apps")
|
||||
self._ensure_directory_with_init(apps_dir, "Created apps/__init__.py")
|
||||
create_directory_with_init(apps_dir, "Created apps/__init__.py")
|
||||
|
||||
# Create apps/core
|
||||
apps_core_dir = os.path.join(apps_dir, "core")
|
||||
self._ensure_directory_with_init(apps_core_dir, "Created apps/core/__init__.py")
|
||||
create_directory_with_init(apps_core_dir, "Created apps/core/__init__.py")
|
||||
|
||||
# Create apps/core/apps.py
|
||||
apps_py_path = os.path.join(apps_core_dir, "apps.py")
|
||||
content = template_engine.render_template("base/apps.j2", {})
|
||||
create_file_with_content(apps_py_path, content, "Created apps/core/apps.py", should_format=True)
|
||||
self._create_apps_py(apps_core_dir, "", "Created apps/core/apps.py", should_format=True)
|
||||
|
||||
# Create apps/core/models
|
||||
models_dir = os.path.join(apps_core_dir, "models")
|
||||
os.makedirs(models_dir, exist_ok=True)
|
||||
model_files = [
|
||||
("__init__.py", "base/models.j2"),
|
||||
("base.py", "unified/apps/core/models/base.py.j2"),
|
||||
("mixins.py", "unified/apps/core/models/mixins.py.j2"),
|
||||
]
|
||||
for filename, template in model_files:
|
||||
filepath = os.path.join(models_dir, filename)
|
||||
content = template_engine.render_template(template, {})
|
||||
create_file_with_content(filepath, content, f"Created apps/core/models/{filename}", should_format=True)
|
||||
os.makedirs(models_dir, exist_ok=True)
|
||||
create_files_from_templates(
|
||||
models_dir, [(f, t, {}) for f, t in model_files], "apps/core/models/", should_format=True
|
||||
)
|
||||
|
||||
# Create apps/core/utils
|
||||
utils_dir = os.path.join(apps_core_dir, "utils")
|
||||
os.makedirs(utils_dir, exist_ok=True)
|
||||
utils_files = [
|
||||
("__init__.py", "unified/apps/core/utils/__init__.py.j2"),
|
||||
("responses.py", "unified/apps/core/utils/responses.py.j2"),
|
||||
]
|
||||
for filename, template in utils_files:
|
||||
filepath = os.path.join(utils_dir, filename)
|
||||
content = template_engine.render_template(template, {})
|
||||
create_file_with_content(filepath, content, f"Created apps/core/utils/{filename}", should_format=True)
|
||||
os.makedirs(utils_dir, exist_ok=True)
|
||||
create_files_from_templates(
|
||||
utils_dir, [(f, t, {}) for f, t in utils_files], "apps/core/utils/", should_format=True
|
||||
)
|
||||
|
||||
# Create apps/core/permissions and middleware
|
||||
for subdir in ["permissions", "middleware"]:
|
||||
subdir_path = os.path.join(apps_core_dir, subdir)
|
||||
self._ensure_directory_with_init(subdir_path, f"Created apps/core/{subdir}/__init__.py")
|
||||
self._create_subdirectories_with_init(apps_core_dir, ["permissions", "middleware"], "apps/core/")
|
||||
|
||||
# Create apps/api
|
||||
apps_api_dir = os.path.join(apps_dir, "api")
|
||||
self._ensure_directory_with_init(apps_api_dir, "Created apps/api/__init__.py")
|
||||
|
||||
# Create apps/api/apps.py
|
||||
apps_py_path = os.path.join(apps_api_dir, "apps.py")
|
||||
content = template_engine.render_template("base/apps.j2", {"app_name": "apps.api"})
|
||||
create_file_with_content(apps_py_path, content, "Created apps/api/apps.py", should_format=True)
|
||||
create_directory_with_init(apps_api_dir, "Created apps/api/__init__.py")
|
||||
self._create_apps_py(apps_api_dir, "apps.api", "Created apps/api/apps.py", should_format=True)
|
||||
|
||||
# Create apps/api subdirectories
|
||||
api_subdirs = ["models", "serializers", "views", "services", "tests", "urls", "admin"]
|
||||
for subdir in api_subdirs:
|
||||
subdir_path = os.path.join(apps_api_dir, subdir)
|
||||
self._ensure_directory_with_init(subdir_path, f"Created apps/api/{subdir}/__init__.py")
|
||||
self._create_subdirectories_with_init(
|
||||
apps_api_dir, ["models", "serializers", "views", "services", "tests", "urls", "admin"], "apps/api/"
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
+119
-299
@@ -13,6 +13,7 @@ from typing import Callable, Tuple
|
||||
|
||||
from djinit.scripts.console_ui import UIColors, UIFormatter, console
|
||||
from djinit.scripts.name_validator import validate_app_name, validate_project_name
|
||||
from djinit.utils import get_package_name
|
||||
|
||||
|
||||
class CICDOption(Enum):
|
||||
@@ -69,7 +70,7 @@ class InputCollector:
|
||||
MAX_ATTEMPTS = 3
|
||||
|
||||
def __init__(self):
|
||||
self.char_reader = CharReader()
|
||||
pass
|
||||
|
||||
def get_validated_input(
|
||||
self, prompt: str, validator: Callable[[str], Tuple[bool, str]], input_type: str, allow_empty: bool = False
|
||||
@@ -204,10 +205,6 @@ class InputCollector:
|
||||
console.print(f" [{UIColors.SUCCESS}]{option.key}[/{UIColors.SUCCESS}] {option.description}")
|
||||
console.print()
|
||||
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
# Get valid keys from CICDOption enum
|
||||
>>>>>>> origin/main
|
||||
valid_keys = [option.key for option in CICDOption]
|
||||
|
||||
choice = CharReader.get_cicd_choice(
|
||||
@@ -216,24 +213,12 @@ class InputCollector:
|
||||
default=CICDOption.NONE.key,
|
||||
)
|
||||
|
||||
<<<<<<< HEAD
|
||||
choice_map = {
|
||||
CICDOption.BOTH.key: (True, True),
|
||||
CICDOption.GITHUB.key: (True, False),
|
||||
CICDOption.GITLAB.key: (False, True),
|
||||
}
|
||||
return choice_map.get(choice, (False, False))
|
||||
=======
|
||||
# Parse choice
|
||||
if choice == CICDOption.BOTH.key:
|
||||
return True, True
|
||||
elif choice == CICDOption.GITHUB.key:
|
||||
return True, False
|
||||
elif choice == CICDOption.GITLAB.key:
|
||||
return False, True
|
||||
else: # Default to none (should not happen due to validation, but kept as fallback)
|
||||
return False, False
|
||||
>>>>>>> origin/main
|
||||
|
||||
def get_nested_apps_config(self) -> Tuple[bool, str | None]:
|
||||
UIFormatter.print_separator()
|
||||
@@ -242,13 +227,7 @@ class InputCollector:
|
||||
)
|
||||
console.print()
|
||||
|
||||
<<<<<<< HEAD
|
||||
choice = CharReader.get_yes_no(f"[{UIColors.HIGHLIGHT}]Nested Django apps? (y/N):[/{UIColors.HIGHLIGHT}]")
|
||||
=======
|
||||
choice = CharReader.get_yes_no(
|
||||
f"[{UIColors.HIGHLIGHT}]Nested Django apps? (y/N):[/{UIColors.HIGHLIGHT}]", default="n"
|
||||
)
|
||||
>>>>>>> origin/main
|
||||
|
||||
if choice != "y":
|
||||
return False, None
|
||||
@@ -268,16 +247,39 @@ class InputCollector:
|
||||
console.print(f" [{UIColors.SUCCESS}]N[/{UIColors.SUCCESS}] Use individual database parameters")
|
||||
console.print()
|
||||
|
||||
<<<<<<< HEAD
|
||||
return CharReader.get_yes_no(f"[{UIColors.HIGHLIGHT}]Use DATABASE_URL? (Y/n):[/{UIColors.HIGHLIGHT}]") == "y"
|
||||
=======
|
||||
choice = CharReader.get_yes_no(
|
||||
f"[{UIColors.HIGHLIGHT}]Use DATABASE_URL? (Y/n):[/{UIColors.HIGHLIGHT}]", default="y"
|
||||
|
||||
def _get_structure_metadata(
|
||||
self, project_dir: str, predefined: bool = False, unified: bool = False
|
||||
) -> Tuple[str, str, list[str], dict]:
|
||||
"""Helper method to get metadata for predefined/unified structures."""
|
||||
project_name = project_dir
|
||||
app_names: list[str] = []
|
||||
|
||||
# Ask for workflows so shared templates for CI/CD can be added
|
||||
use_github, use_gitlab = self.get_cicd_choice()
|
||||
|
||||
# Ask database configuration
|
||||
use_database_url = self.get_database_config_choice()
|
||||
|
||||
# Default package_name to "backend" if project_dir is "." or empty
|
||||
package_name = get_package_name(project_dir)
|
||||
|
||||
project_module_name = "config" if predefined else "core" if unified else None
|
||||
|
||||
metadata = ProjectMetadata(
|
||||
package_name=package_name,
|
||||
use_github_actions=use_github,
|
||||
use_gitlab_ci=use_gitlab,
|
||||
nested_apps=True,
|
||||
nested_dir="apps",
|
||||
use_database_url=use_database_url,
|
||||
predefined_structure=predefined,
|
||||
unified_structure=unified,
|
||||
project_module_name=project_module_name,
|
||||
)
|
||||
|
||||
# Return True if choice is 'y', False if 'n'
|
||||
return choice == "y"
|
||||
>>>>>>> origin/main
|
||||
return project_name, "", app_names, metadata.to_dict()
|
||||
|
||||
|
||||
class CharReader:
|
||||
@@ -290,7 +292,6 @@ class CharReader:
|
||||
return CharReader._get_char_unix()
|
||||
|
||||
@staticmethod
|
||||
<<<<<<< HEAD
|
||||
def _is_enter_key(response: str) -> bool:
|
||||
"""Check if response represents Enter key or control character."""
|
||||
return not response or response in ("\r", "\n") or ord(response) < 32
|
||||
@@ -323,30 +324,10 @@ class CharReader:
|
||||
# Handle Enter key (default)
|
||||
if CharReader._is_enter_key(response):
|
||||
return CharReader._handle_default(default)
|
||||
=======
|
||||
def get_yes_no(prompt: str, default: str = "n") -> str:
|
||||
max_attempts = 3
|
||||
attempt = 0
|
||||
|
||||
while attempt < max_attempts:
|
||||
try:
|
||||
console.print(f"{prompt} ", end="")
|
||||
sys.stdout.flush()
|
||||
|
||||
reader = CharReader()
|
||||
response = reader.get_char()
|
||||
|
||||
# Handle Enter key (default) - in raw mode, Enter is '\r' (carriage return)
|
||||
# Also handle empty string or other control characters as Enter
|
||||
if not response or response == "\r" or response == "\n" or ord(response) < 32:
|
||||
console.print(default.upper())
|
||||
return default.lower()
|
||||
>>>>>>> origin/main
|
||||
|
||||
response = response.lower()
|
||||
|
||||
# Validate response
|
||||
<<<<<<< HEAD
|
||||
if response in valid_keys_lower:
|
||||
console.print(response.upper())
|
||||
return response
|
||||
@@ -359,30 +340,11 @@ class CharReader:
|
||||
if attempt < CharReader.MAX_ATTEMPTS:
|
||||
console.print(
|
||||
f"[{UIColors.MUTED}]Please try again ({attempt}/{CharReader.MAX_ATTEMPTS}).[/{UIColors.MUTED}]"
|
||||
=======
|
||||
if response in ("y", "n"):
|
||||
console.print(response.upper())
|
||||
return response
|
||||
|
||||
# Invalid input - show error (only show if it's a printable character)
|
||||
console.print()
|
||||
if response.isprintable():
|
||||
UIFormatter.print_error(f"Invalid input '{response}'. Please enter 'y' or 'n'.")
|
||||
attempt += 1
|
||||
if attempt < max_attempts:
|
||||
console.print(
|
||||
f"[{UIColors.MUTED}]Please try again ({attempt}/{max_attempts}).[/{UIColors.MUTED}]"
|
||||
>>>>>>> origin/main
|
||||
)
|
||||
console.print()
|
||||
else:
|
||||
# Control character entered, treat as Enter
|
||||
<<<<<<< HEAD
|
||||
return CharReader._handle_default(default)
|
||||
=======
|
||||
console.print(default.upper())
|
||||
return default.lower()
|
||||
>>>>>>> origin/main
|
||||
|
||||
except KeyboardInterrupt:
|
||||
console.print()
|
||||
@@ -395,7 +357,6 @@ class CharReader:
|
||||
return default.lower()
|
||||
|
||||
@staticmethod
|
||||
<<<<<<< HEAD
|
||||
def get_yes_no(prompt: str, default: str = None) -> str:
|
||||
# Auto-detect default from prompt format (capital letter indicates default)
|
||||
if default is None:
|
||||
@@ -423,70 +384,16 @@ class CharReader:
|
||||
default=default,
|
||||
error_message=f"Invalid input '{{response}}'. Please enter one of: {valid_keys_str}.",
|
||||
)
|
||||
=======
|
||||
def get_cicd_choice(prompt: str, valid_keys: list[str], default: str = "n") -> str:
|
||||
"""
|
||||
Get CI/CD choice input with validation and retries.
|
||||
|
||||
Args:
|
||||
prompt: The prompt message to display
|
||||
valid_keys: List of valid key characters (e.g., ['g', 'l', 'b', 'n'])
|
||||
default: Default value if Enter is pressed
|
||||
|
||||
Returns:
|
||||
Valid key (lowercase)
|
||||
"""
|
||||
max_attempts = 3
|
||||
attempt = 0
|
||||
valid_keys_lower = [k.lower() for k in valid_keys]
|
||||
|
||||
while attempt < max_attempts:
|
||||
try:
|
||||
console.print(f"{prompt} ", end="")
|
||||
sys.stdout.flush()
|
||||
|
||||
reader = CharReader()
|
||||
response = reader.get_char()
|
||||
|
||||
# Handle Enter key (default) - in raw mode, Enter is '\r' (carriage return)
|
||||
# Also handle empty string or other control characters as Enter
|
||||
if not response or response == "\r" or response == "\n" or ord(response) < 32:
|
||||
console.print(default.upper())
|
||||
return default.lower()
|
||||
|
||||
response = response.lower()
|
||||
|
||||
# Validate response
|
||||
if response in valid_keys_lower:
|
||||
console.print(response.upper())
|
||||
return response
|
||||
|
||||
# Invalid input - show error (only show if it's a printable character)
|
||||
console.print()
|
||||
if response.isprintable():
|
||||
valid_keys_str = ", ".join(f"'{k}'" for k in valid_keys)
|
||||
UIFormatter.print_error(f"Invalid input '{response}'. Please enter one of: {valid_keys_str}.")
|
||||
attempt += 1
|
||||
if attempt < max_attempts:
|
||||
console.print(
|
||||
f"[{UIColors.MUTED}]Please try again ({attempt}/{max_attempts}).[/{UIColors.MUTED}]"
|
||||
)
|
||||
console.print()
|
||||
else:
|
||||
# Control character entered, treat as Enter
|
||||
console.print(default.upper())
|
||||
return default.lower()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
console.print()
|
||||
UIFormatter.print_info("\nOperation cancelled by user.")
|
||||
raise
|
||||
|
||||
# Max attempts reached - use default
|
||||
console.print()
|
||||
UIFormatter.print_warning(f"Maximum attempts reached. Using default: '{default.upper()}'.")
|
||||
return default.lower()
|
||||
>>>>>>> origin/main
|
||||
@staticmethod
|
||||
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"],
|
||||
default="1",
|
||||
error_message="Invalid input '{response}'. Please enter '1', '2', or '3'.",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _get_char_windows() -> str:
|
||||
@@ -497,13 +404,32 @@ class CharReader:
|
||||
for encoding in encodings:
|
||||
try:
|
||||
ch = msvcrt.getch()
|
||||
return ch.decode(encoding).lower()
|
||||
# Detect Ctrl+C (ETX character \x03)
|
||||
if isinstance(ch, bytes):
|
||||
if ch == b"\x03":
|
||||
raise KeyboardInterrupt
|
||||
decoded = ch.decode(encoding).lower()
|
||||
else:
|
||||
if ord(ch) == 3: # Ctrl+C
|
||||
raise KeyboardInterrupt
|
||||
decoded = ch.lower()
|
||||
return decoded
|
||||
except (UnicodeDecodeError, AttributeError):
|
||||
continue
|
||||
|
||||
# Fallback
|
||||
try:
|
||||
return msvcrt.getch().lower()
|
||||
ch = msvcrt.getch()
|
||||
if isinstance(ch, bytes):
|
||||
if ch == b"\x03":
|
||||
raise KeyboardInterrupt
|
||||
return ch.decode("utf-8", errors="ignore").lower()
|
||||
else:
|
||||
if ord(ch) == 3: # Ctrl+C
|
||||
raise KeyboardInterrupt
|
||||
return ch.lower()
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
@@ -514,201 +440,101 @@ class CharReader:
|
||||
try:
|
||||
tty.setraw(sys.stdin.fileno())
|
||||
ch = sys.stdin.read(1)
|
||||
# Detect Ctrl+C (ETX character \x03)
|
||||
if ord(ch) == 3: # Ctrl+C
|
||||
raise KeyboardInterrupt
|
||||
return ch.lower()
|
||||
finally:
|
||||
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
||||
|
||||
|
||||
def get_user_input() -> Tuple[str, str, str, list, dict]:
|
||||
collector = InputCollector()
|
||||
console.print()
|
||||
try:
|
||||
collector = InputCollector()
|
||||
console.print()
|
||||
|
||||
# Pre-step: Ask for structure type first
|
||||
UIFormatter.print_separator()
|
||||
<<<<<<< HEAD
|
||||
console.print(f"\n[{UIColors.INFO}]Choose Structure Type[/{UIColors.INFO}]\n")
|
||||
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"\n[{UIColors.INFO}]Step 1: Project Setup[/{UIColors.INFO}]\n")
|
||||
# Pre-step: Ask for structure type first
|
||||
UIFormatter.print_separator()
|
||||
console.print(f"\n[{UIColors.INFO}]Choose Structure Type[/{UIColors.INFO}]\n")
|
||||
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()
|
||||
|
||||
console.print(f"[{UIColors.HIGHLIGHT}]Project Directory[/{UIColors.HIGHLIGHT}]")
|
||||
console.print(f"[{UIColors.MUTED}]Where your project files will be created[/{UIColors.MUTED}]")
|
||||
console.print(f"[{UIColors.MUTED}]Press Enter or enter '.' to create in current directory[/{UIColors.MUTED}]")
|
||||
structure_choice = CharReader.get_structure_choice()
|
||||
|
||||
# Get project directory with special handling for '.' and empty input
|
||||
attempt = 0
|
||||
project_dir = None
|
||||
use_predefined = structure_choice == "2"
|
||||
use_unified = structure_choice == "3"
|
||||
|
||||
while attempt < collector.MAX_ATTEMPTS and project_dir is None:
|
||||
try:
|
||||
user_input = console.input(
|
||||
f"[{UIColors.HIGHLIGHT}]Enter project directory name:[/{UIColors.HIGHLIGHT}] "
|
||||
).strip()
|
||||
if use_predefined or use_unified:
|
||||
UIFormatter.print_separator()
|
||||
console.print(f"\n[{UIColors.INFO}]Step 1: Project Setup[/{UIColors.INFO}]\n")
|
||||
console.print(
|
||||
f"[{UIColors.MUTED}]Press Enter or enter '.' to create in current directory[/{UIColors.MUTED}]"
|
||||
)
|
||||
project_dir = collector._get_project_directory()
|
||||
if project_dir is None:
|
||||
UIFormatter.print_error("Maximum attempts reached for project directory name. Exiting.")
|
||||
sys.exit(1)
|
||||
|
||||
# Handle empty input or '.' - use current directory
|
||||
if not user_input or user_input == ".":
|
||||
project_dir = "."
|
||||
UIFormatter.print_info(f"Creating project in current directory: {os.getcwd()}")
|
||||
break
|
||||
project_name, primary_app, app_names, metadata_dict = collector._get_structure_metadata(
|
||||
project_dir, predefined=use_predefined, unified=use_unified
|
||||
)
|
||||
|
||||
# Validate the input
|
||||
is_valid, error_msg = validate_project_name(user_input)
|
||||
if not is_valid:
|
||||
UIFormatter.print_error(error_msg)
|
||||
attempt += 1
|
||||
collector._show_retry_message(attempt)
|
||||
continue
|
||||
return project_dir, project_name, primary_app, app_names, metadata_dict
|
||||
|
||||
# Check if directory exists and ask for alternative
|
||||
if os.path.exists(user_input):
|
||||
UIFormatter.print_error(f"Directory '{user_input}' already exists.")
|
||||
UIFormatter.print_info(
|
||||
"Please choose a different project directory name (or press Enter for current directory)."
|
||||
)
|
||||
attempt += 1
|
||||
collector._show_retry_message(attempt)
|
||||
continue
|
||||
|
||||
project_dir = user_input
|
||||
break
|
||||
|
||||
except KeyboardInterrupt:
|
||||
UIFormatter.print_info("\nSetup cancelled by user.")
|
||||
sys.exit(0)
|
||||
|
||||
if project_dir is None:
|
||||
UIFormatter.print_error("Maximum attempts reached for project directory name. Exiting.")
|
||||
sys.exit(1)
|
||||
|
||||
>>>>>>> origin/main
|
||||
console.print()
|
||||
|
||||
structure_choice = console.input(
|
||||
f"[{UIColors.HIGHLIGHT}]Choose structure type (1/2/3) [default: 1]:[/{UIColors.HIGHLIGHT}] "
|
||||
).strip()
|
||||
if not structure_choice:
|
||||
structure_choice = "1"
|
||||
|
||||
use_predefined = structure_choice == "2"
|
||||
use_unified = structure_choice == "3"
|
||||
|
||||
if use_predefined:
|
||||
# Section 1: Project Setup (standard flow)
|
||||
UIFormatter.print_separator()
|
||||
console.print(f"\n[{UIColors.INFO}]Step 1: Project Setup[/{UIColors.INFO}]\n")
|
||||
console.print(f"[{UIColors.MUTED}]Press Enter or enter '.' to create in current directory[/{UIColors.MUTED}]")
|
||||
|
||||
project_dir = collector._get_project_directory()
|
||||
|
||||
if project_dir is None:
|
||||
UIFormatter.print_error("Maximum attempts reached for project directory name. Exiting.")
|
||||
sys.exit(1)
|
||||
|
||||
# Minimal inputs: project_dir only, then ask for CI/CD choice.
|
||||
project_name = project_dir
|
||||
app_names: list[str] = []
|
||||
console.print()
|
||||
|
||||
# Ask for workflows so shared templates for CI/CD can be added
|
||||
use_github, use_gitlab = collector.get_cicd_choice()
|
||||
|
||||
# Ask database configuration for predefined structure as well
|
||||
use_database_url = collector.get_database_config_choice()
|
||||
# Default package_name to "backend" if project_dir is "." or empty
|
||||
package_name = "backend" if project_dir == "." or not project_dir else project_dir
|
||||
metadata = ProjectMetadata(
|
||||
package_name=package_name,
|
||||
use_github_actions=use_github,
|
||||
use_gitlab_ci=use_gitlab,
|
||||
nested_apps=True,
|
||||
nested_dir="apps",
|
||||
use_database_url=use_database_url,
|
||||
predefined_structure=True,
|
||||
project_module_name="config",
|
||||
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"
|
||||
)
|
||||
console.print()
|
||||
|
||||
return project_dir, project_name, "", app_names, metadata.to_dict()
|
||||
|
||||
if use_unified:
|
||||
# Section 2: Django Apps
|
||||
console.print()
|
||||
UIFormatter.print_separator()
|
||||
console.print(f"\n[{UIColors.INFO}]Step 1: Project Setup[/{UIColors.INFO}]\n")
|
||||
console.print(f"[{UIColors.MUTED}]Press Enter or enter '.' to create in current directory[/{UIColors.MUTED}]")
|
||||
project_dir = collector._get_project_directory()
|
||||
if project_dir is None:
|
||||
UIFormatter.print_error("Maximum attempts reached for project directory name. Exiting.")
|
||||
sys.exit(1)
|
||||
console.print(f"\n[{UIColors.INFO}]Step 2: Django Apps[/{UIColors.INFO}]\n")
|
||||
nested, nested_dir = collector.get_nested_apps_config()
|
||||
app_names = collector.get_app_names()
|
||||
console.print()
|
||||
|
||||
# Minimal inputs: project_dir only, then ask for CI/CD choice.
|
||||
project_name = project_dir
|
||||
app_names: list[str] = []
|
||||
|
||||
# Ask for workflows so shared templates for CI/CD can be added
|
||||
# Section 3: CI/CD Pipeline
|
||||
use_github, use_gitlab = collector.get_cicd_choice()
|
||||
|
||||
# Ask database configuration for unified structure as well
|
||||
# Section 4: Database Configuration
|
||||
use_database_url = collector.get_database_config_choice()
|
||||
|
||||
# Create metadata
|
||||
# Default package_name to "backend" if project_dir is "." or empty
|
||||
package_name = "backend" if project_dir == "." or not project_dir else project_dir
|
||||
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=True,
|
||||
nested_dir="apps",
|
||||
nested_apps=nested,
|
||||
nested_dir=nested_dir,
|
||||
use_database_url=use_database_url,
|
||||
unified_structure=True,
|
||||
project_module_name="core",
|
||||
)
|
||||
|
||||
return project_dir, project_name, "", app_names, metadata.to_dict()
|
||||
console.print()
|
||||
|
||||
# Section 1: Project Setup (standard flow)
|
||||
UIFormatter.print_separator()
|
||||
console.print(f"\n[{UIColors.INFO}]Step 1: Project Setup[/{UIColors.INFO}]\n")
|
||||
console.print(f"[{UIColors.MUTED}]Press Enter or enter '.' to create in current directory[/{UIColors.MUTED}]")
|
||||
|
||||
project_dir = collector._get_project_directory()
|
||||
|
||||
if project_dir is None:
|
||||
UIFormatter.print_error("Maximum attempts reached for project directory name. Exiting.")
|
||||
sys.exit(1)
|
||||
|
||||
console.print()
|
||||
|
||||
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"
|
||||
)
|
||||
console.print()
|
||||
|
||||
# Section 2: Django Apps
|
||||
console.print()
|
||||
UIFormatter.print_separator()
|
||||
console.print(f"\n[{UIColors.INFO}]Step 2: Django Apps[/{UIColors.INFO}]\n")
|
||||
nested, nested_dir = collector.get_nested_apps_config()
|
||||
app_names = collector.get_app_names()
|
||||
console.print()
|
||||
|
||||
# Section 3: CI/CD Pipeline
|
||||
use_github, use_gitlab = collector.get_cicd_choice()
|
||||
|
||||
# Section 4: Database Configuration
|
||||
use_database_url = collector.get_database_config_choice()
|
||||
|
||||
# Create metadata
|
||||
# Default package_name to "backend" if project_dir is "." or empty
|
||||
package_name = "backend" if project_dir == "." or not project_dir else 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,
|
||||
)
|
||||
|
||||
console.print()
|
||||
|
||||
# Return in legacy format for backward compatibility
|
||||
return project_dir, project_name, app_names[0], app_names, metadata.to_dict()
|
||||
# Return in legacy format for backward compatibility
|
||||
return project_dir, project_name, app_names[0], app_names, metadata.to_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:
|
||||
@@ -734,13 +560,7 @@ def confirm_setup(project_dir: str, project_name: str, app_names: list, metadata
|
||||
console.print()
|
||||
|
||||
try:
|
||||
<<<<<<< HEAD
|
||||
response = CharReader.get_yes_no(f"[{UIColors.WARNING}]Proceed with setup? (y/N):[/{UIColors.WARNING}]")
|
||||
=======
|
||||
response = CharReader.get_yes_no(
|
||||
f"[{UIColors.WARNING}]Proceed with setup? (y/N):[/{UIColors.WARNING}]", default="n"
|
||||
)
|
||||
>>>>>>> origin/main
|
||||
return response == "y"
|
||||
|
||||
except KeyboardInterrupt:
|
||||
|
||||
+13
-37
@@ -9,6 +9,7 @@ from djinit.scripts.console_ui import UIFormatter
|
||||
from djinit.scripts.django_helper import DjangoHelper
|
||||
from djinit.utils import (
|
||||
calculate_app_module_paths,
|
||||
create_directory_with_init,
|
||||
extract_existing_apps,
|
||||
get_base_settings_path,
|
||||
insert_apps_into_user_defined_apps,
|
||||
@@ -22,23 +23,14 @@ class ProjectManager:
|
||||
self.project_name = project_name
|
||||
self.app_names = app_names
|
||||
self.metadata = metadata
|
||||
<<<<<<< HEAD
|
||||
self.project_root = os.getcwd() if project_dir == "." else os.path.join(os.getcwd(), project_dir)
|
||||
# Support custom module name for the Django config package (e.g., 'config')
|
||||
# Fall back to project_name if key present but None
|
||||
self.module_name = metadata.get("project_module_name") or self.project_name
|
||||
=======
|
||||
# Handle '.' for current directory
|
||||
if project_dir == ".":
|
||||
self.project_root = os.getcwd()
|
||||
else:
|
||||
self.project_root = os.path.join(os.getcwd(), project_dir)
|
||||
>>>>>>> origin/main
|
||||
|
||||
def create_project(self) -> bool:
|
||||
os.makedirs(self.project_root, exist_ok=True)
|
||||
|
||||
<<<<<<< HEAD
|
||||
unified = self.metadata.get("unified_structure", False)
|
||||
success = DjangoHelper.startproject(self.module_name, self.project_root, unified=unified)
|
||||
if success:
|
||||
@@ -46,29 +38,19 @@ class ProjectManager:
|
||||
else:
|
||||
UIFormatter.print_error(f"Failed to create Django project '{self.project_name}'")
|
||||
return success
|
||||
=======
|
||||
# Create Django project inside the directory
|
||||
# Use 'python -m django' instead of 'django-admin' for better compatibility
|
||||
subprocess.run(
|
||||
[sys.executable, "-m", "django", "startproject", self.project_name, self.project_root],
|
||||
check=True,
|
||||
)
|
||||
|
||||
UIFormatter.print_success(f"Django project '{self.project_name}' created successfully!")
|
||||
return True
|
||||
>>>>>>> origin/main
|
||||
def _get_apps_base_dir(self) -> str:
|
||||
"""Get the base directory for apps, considering nested structure."""
|
||||
apps_base_dir = self.project_root
|
||||
if self.metadata.get("nested_apps") and self.metadata.get("nested_dir"):
|
||||
apps_base_dir = os.path.join(self.project_root, self.metadata.get("nested_dir"))
|
||||
return apps_base_dir
|
||||
|
||||
def create_apps(self) -> bool:
|
||||
apps_base_dir = self.project_root
|
||||
nested = bool(self.metadata.get("nested_apps"))
|
||||
nested_dir_name = self.metadata.get("nested_dir") if nested else None
|
||||
apps_base_dir = self._get_apps_base_dir()
|
||||
|
||||
if nested and nested_dir_name:
|
||||
apps_base_dir = os.path.join(self.project_root, nested_dir_name)
|
||||
os.makedirs(apps_base_dir, exist_ok=True)
|
||||
init_file = os.path.join(apps_base_dir, "__init__.py")
|
||||
if not os.path.exists(init_file):
|
||||
open(init_file, "a").close()
|
||||
if apps_base_dir != self.project_root:
|
||||
create_directory_with_init(apps_base_dir, f"Created {os.path.basename(apps_base_dir)}/__init__.py")
|
||||
|
||||
for app_name in self.app_names:
|
||||
success = DjangoHelper.startapp(app_name, apps_base_dir)
|
||||
@@ -78,12 +60,12 @@ class ProjectManager:
|
||||
UIFormatter.print_success(f"Django app '{app_name}' created successfully!")
|
||||
|
||||
# Add all apps to USER_DEFINED_APPS in settings
|
||||
if not self._add_apps_to_settings():
|
||||
if not self.add_apps_to_settings():
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _add_apps_to_settings(self) -> bool:
|
||||
def add_apps_to_settings(self) -> bool:
|
||||
"""Add all apps to USER_DEFINED_APPS in base.py settings file."""
|
||||
base_settings_path = get_base_settings_path(self.project_root, self.module_name)
|
||||
|
||||
@@ -116,10 +98,6 @@ class ProjectManager:
|
||||
UIFormatter.print_success(f"Added apps to USER_DEFINED_APPS: {added_apps_str}")
|
||||
return True
|
||||
|
||||
# Public wrapper to allow external callers to add apps based on current state
|
||||
def add_apps_to_settings(self) -> bool:
|
||||
return self._add_apps_to_settings()
|
||||
|
||||
def validate_project_structure(self) -> bool:
|
||||
required_files = [
|
||||
os.path.join(self.project_root, "manage.py"),
|
||||
@@ -133,9 +111,7 @@ class ProjectManager:
|
||||
os.path.join(self.project_root, self.module_name, "asgi.py"),
|
||||
]
|
||||
|
||||
apps_base_dir = self.project_root
|
||||
if self.metadata.get("nested_apps") and self.metadata.get("nested_dir"):
|
||||
apps_base_dir = os.path.join(self.project_root, self.metadata.get("nested_dir"))
|
||||
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"):
|
||||
|
||||
@@ -5,5 +5,3 @@ app_name = "api"
|
||||
urlpatterns = [
|
||||
path("v1/", include("api.v1.urls")),
|
||||
]
|
||||
|
||||
|
||||
@@ -5,5 +5,3 @@ app_name = "v1"
|
||||
urlpatterns = [
|
||||
path("users/", include("apps.users.urls")),
|
||||
]
|
||||
|
||||
|
||||
@@ -6,5 +6,3 @@ class {{ model_class_name }}(models.Model):
|
||||
|
||||
# Add fields here
|
||||
pass
|
||||
|
||||
|
||||
@@ -7,5 +7,3 @@ class {{ model_class_name }}Serializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = {{ model_class_name }}
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
@@ -8,5 +8,3 @@ def get_{{ app_name }}_by_id(object_id: int) -> Optional[{{ model_class_name }}]
|
||||
return {{ model_class_name }}.objects.get(id=object_id)
|
||||
except {{ model_class_name }}.DoesNotExist:
|
||||
return None
|
||||
|
||||
|
||||
@@ -10,5 +10,3 @@ def test_{{ app_name }}_list_empty():
|
||||
response = client.get(url)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == []
|
||||
|
||||
|
||||
@@ -12,5 +12,3 @@ router.register(r"{{ app_name }}", {{ model_class_name }}ViewSet, basename="{{ a
|
||||
urlpatterns = [
|
||||
path("", include(router.urls)),
|
||||
]
|
||||
|
||||
|
||||
@@ -7,5 +7,3 @@ from {{ app_module }}.serializers.{{ app_name }}_serializer import {{ model_clas
|
||||
class {{ model_class_name }}ViewSet(viewsets.ModelViewSet):
|
||||
queryset = {{ model_class_name }}.objects.all()
|
||||
serializer_class = {{ model_class_name }}Serializer
|
||||
|
||||
|
||||
@@ -7,5 +7,3 @@ class User(AbstractUser):
|
||||
|
||||
# Add custom fields here if needed
|
||||
pass
|
||||
|
||||
|
||||
@@ -7,5 +7,3 @@ class UserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ["id", "username", "email", "first_name", "last_name"]
|
||||
|
||||
|
||||
@@ -8,5 +8,3 @@ def get_user_by_id(user_id: int) -> Optional[User]:
|
||||
return User.objects.get(id=user_id)
|
||||
except User.DoesNotExist:
|
||||
return None
|
||||
|
||||
|
||||
@@ -10,5 +10,3 @@ def test_users_list_empty():
|
||||
response = client.get(url)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == []
|
||||
|
||||
|
||||
@@ -12,5 +12,3 @@ router.register(r"users", UserViewSet, basename="users")
|
||||
urlpatterns = [
|
||||
path("", include(router.urls)),
|
||||
]
|
||||
|
||||
|
||||
@@ -11,5 +11,3 @@ class UserViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
return super().list(request, *args, **kwargs)
|
||||
|
||||
|
||||
@@ -3,5 +3,3 @@ class ApplicationError(Exception):
|
||||
|
||||
def __init__(self, message: str):
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
@@ -12,5 +12,3 @@ class RequestLoggerMiddleware:
|
||||
logger.info("%s %s", request.method, request.path)
|
||||
response = self.get_response(request)
|
||||
return response
|
||||
|
||||
|
||||
@@ -7,5 +7,3 @@ class TimestampedModel(models.Model):
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
@@ -7,5 +7,3 @@ def success(data=None, status_code=200):
|
||||
|
||||
def error(message, status_code=400):
|
||||
return Response({"success": False, "message": message}, status=status_code)
|
||||
|
||||
|
||||
@@ -26,4 +26,4 @@ if settings.DEBUG:
|
||||
urlpatterns += [
|
||||
path("schema/", SpectacularAPIView.as_view(), name="schema"),
|
||||
path("docs/", SpectacularSwaggerView.as_view(url_name="schema")),
|
||||
]
|
||||
]
|
||||
@@ -6,4 +6,3 @@ from django.apps import AppConfig
|
||||
class CoreConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.core"
|
||||
|
||||
@@ -2,4 +2,3 @@ from .base import AbstractBaseModel
|
||||
from .mixins import TimestampedModel
|
||||
|
||||
__all__ = ["AbstractBaseModel", "TimestampedModel"]
|
||||
|
||||
@@ -11,4 +11,3 @@ class AbstractBaseModel(models.Model):
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
@@ -11,4 +11,3 @@ class TimestampedModel(models.Model):
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from .responses import success, error
|
||||
|
||||
__all__ = ["success", "error"]
|
||||
|
||||
@@ -11,4 +11,3 @@ def success(data=None, status_code=200):
|
||||
def error(message, status_code=400):
|
||||
"""Return an error response."""
|
||||
return Response({"success": False, "message": message}, status=status_code)
|
||||
|
||||
+67
-31
@@ -14,10 +14,6 @@ from djinit.scripts.template_engine import template_engine
|
||||
|
||||
def format_file(filename: str) -> None:
|
||||
"""Format Python file using Ruff formatter."""
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
# Use 'python -m ruff' instead of 'ruff' for better compatibility
|
||||
>>>>>>> origin/main
|
||||
subprocess.run([sys.executable, "-m", "ruff", "format", filename], check=False, capture_output=True)
|
||||
|
||||
|
||||
@@ -51,6 +47,38 @@ def create_init_file(directory: str, success_message: str) -> None:
|
||||
create_file_from_template(init_path, "shared/init.j2", {}, success_message, should_format=False)
|
||||
|
||||
|
||||
def create_directory_with_init(dir_path: str, message: str) -> None:
|
||||
"""Create directory and __init__.py file."""
|
||||
os.makedirs(dir_path, exist_ok=True)
|
||||
create_init_file(dir_path, message)
|
||||
|
||||
|
||||
def create_files_from_templates(base_dir: str, files: list, prefix: str = "", should_format: bool = False) -> None:
|
||||
"""Create multiple files from templates.
|
||||
|
||||
Args:
|
||||
base_dir: Base directory where files will be created
|
||||
files: List of tuples (filename, template_path, context)
|
||||
prefix: Optional prefix for success message
|
||||
should_format: Whether to format the created files
|
||||
"""
|
||||
for file_spec in files:
|
||||
if len(file_spec) == 3:
|
||||
filename, template_path, context = file_spec
|
||||
else:
|
||||
# Support (filename, template_path) format with empty context
|
||||
filename, template_path = file_spec
|
||||
context = {}
|
||||
filepath = os.path.join(base_dir, filename)
|
||||
message = f"Created {prefix}{filename}" if prefix else f"Created {filename}"
|
||||
create_file_from_template(filepath, template_path, context, message, should_format=should_format)
|
||||
|
||||
|
||||
def get_package_name(project_dir: str) -> str:
|
||||
"""Get package name, defaulting to 'backend' if project_dir is '.' or empty."""
|
||||
return "backend" if project_dir == "." or not project_dir else project_dir
|
||||
|
||||
|
||||
# change the current working dir tempporary
|
||||
@contextmanager
|
||||
def change_cwd(path: str):
|
||||
@@ -62,24 +90,41 @@ def change_cwd(path: str):
|
||||
os.chdir(original_cwd)
|
||||
|
||||
|
||||
# get app names from USER_DEFINED_APPS section in project_name/settings/base.py
|
||||
def extract_existing_apps(content: str) -> set:
|
||||
def _parse_app_entry_from_line(line: str) -> str | None:
|
||||
"""Extract app name from a line in USER_DEFINED_APPS section."""
|
||||
line_stripped = line.strip().strip(",").strip()
|
||||
for quote in ('"', "'"):
|
||||
if line_stripped.startswith(quote) and line_stripped.endswith(quote):
|
||||
return line_stripped.strip(quote)
|
||||
return None
|
||||
|
||||
|
||||
def _is_user_apps_section_start(line: str) -> bool:
|
||||
"""Check if line starts USER_DEFINED_APPS section."""
|
||||
return "USER_DEFINED_APPS" in line and "=" in line
|
||||
|
||||
|
||||
def _iterate_user_apps_lines(content: str):
|
||||
"""Generator that yields lines within USER_DEFINED_APPS section."""
|
||||
lines = content.split("\n")
|
||||
existing_apps = set()
|
||||
in_user_apps = False
|
||||
|
||||
for line in lines:
|
||||
if "USER_DEFINED_APPS" in line and "=" in line:
|
||||
if _is_user_apps_section_start(line):
|
||||
in_user_apps = True
|
||||
elif in_user_apps and line.strip() == "]":
|
||||
break
|
||||
elif in_user_apps:
|
||||
line_stripped = line.strip().strip(",").strip()
|
||||
for quote in ('"', "'"):
|
||||
if line_stripped.startswith(quote) and line_stripped.endswith(quote):
|
||||
existing_apps.add(line_stripped.strip(quote))
|
||||
break
|
||||
yield line
|
||||
|
||||
|
||||
# get app names from USER_DEFINED_APPS section in project_name/settings/base.py
|
||||
def extract_existing_apps(content: str) -> set:
|
||||
existing_apps = set()
|
||||
for line in _iterate_user_apps_lines(content):
|
||||
app_name = _parse_app_entry_from_line(line)
|
||||
if app_name:
|
||||
existing_apps.add(app_name)
|
||||
return existing_apps
|
||||
|
||||
|
||||
@@ -123,7 +168,7 @@ def insert_apps_into_user_defined_apps(content: str, apps_to_add: list) -> str:
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
# Entering USER_DEFINED_APPS section
|
||||
if "USER_DEFINED_APPS" in line and "=" in line and not apps_added:
|
||||
if _is_user_apps_section_start(line) and not apps_added:
|
||||
in_user_apps = True
|
||||
|
||||
# Handle closing bracket on same line
|
||||
@@ -166,7 +211,7 @@ def insert_apps_into_user_defined_apps(content: str, apps_to_add: list) -> str:
|
||||
def calculate_app_module_paths(app_names: list, metadata: dict) -> list:
|
||||
nested = bool(metadata.get("nested_apps"))
|
||||
nested_dir_name = metadata.get("nested_dir") if nested else None
|
||||
return [f"{nested_dir_name}.{app_name}" if nested and nested_dir_name else app_name for app_name in app_names]
|
||||
return [calculate_app_module_path(app_name, nested, nested_dir_name) for app_name in app_names]
|
||||
|
||||
|
||||
# calculate app module path for a single app
|
||||
@@ -234,22 +279,13 @@ def detect_nested_structure_from_settings(
|
||||
with open(base_settings_path) as f:
|
||||
base_content = f.read()
|
||||
|
||||
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:
|
||||
nested_dir_name = app_label.split(".", 1)[0]
|
||||
nested_path = os.path.join(search_dir, nested_dir_name)
|
||||
if os.path.isdir(nested_path):
|
||||
return True, nested_dir_name, nested_path
|
||||
for line in _iterate_user_apps_lines(base_content):
|
||||
app_label = _parse_app_entry_from_line(line)
|
||||
if app_label and "." in app_label:
|
||||
nested_dir_name = app_label.split(".", 1)[0]
|
||||
nested_path = os.path.join(search_dir, nested_dir_name)
|
||||
if os.path.isdir(nested_path):
|
||||
return True, nested_dir_name, nested_path
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
Reference in new issue
Block a user