refactor(structure): streamline project structure creation and file generation

This commit is contained in:
sankalp committed 2025-11-09 15:10:09 +05:45
1 parent cdaeede45a
commit 30ac34478b
32 files changed
+334 -592

No files matched your search

-5
View File
@@ -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)
+39 -53
View File
@@ -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
+30 -58
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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"):
-2
View File
@@ -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)
+1 -1
View File
@@ -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
View File
@@ -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