feat: add predefined project structure option with core and users apps
This commit is contained in:
31 files changed
+637
-51
No files matched your search
+35
-11
@@ -28,17 +28,41 @@ class Cli:
|
||||
self.file_manager = FileManager(self.project_root, project_name, app_names, metadata)
|
||||
|
||||
def run_setup(self) -> bool:
|
||||
steps: List[Tuple[str, Callable[[], bool]]] = [
|
||||
("Creating Django project", self.project_manager.create_project),
|
||||
("Creating Django apps", self.project_manager.create_apps),
|
||||
("Creating project URLs", self.file_manager.create_project_urls),
|
||||
("Validating project structure", self.project_manager.validate_project_structure),
|
||||
("Creating utility files", self._create_utility_files),
|
||||
("Creating Procfile", self.file_manager.create_procfile),
|
||||
("Creating Justfile", self.file_manager.create_justfile),
|
||||
("Creating runtime.txt", self.file_manager.create_runtime_txt),
|
||||
("Creating CI/CD pipelines", self._create_cicd_pipelines),
|
||||
]
|
||||
# If predefined structure is enabled, adjust metadata defaults
|
||||
if self.metadata.get("predefined_structure"):
|
||||
# Default module name to 'config' for conventional layout
|
||||
self.metadata.setdefault("project_module_name", "config")
|
||||
# Ensure nested apps live under 'apps'
|
||||
self.metadata.setdefault("nested_apps", True)
|
||||
self.metadata.setdefault("nested_dir", "apps")
|
||||
# Default apps for predefined structure
|
||||
if not self.app_names:
|
||||
self.app_names = ["users", "core"]
|
||||
# propagate to managers
|
||||
self.project_manager.app_names = self.app_names
|
||||
|
||||
steps: List[Tuple[str, Callable[[], bool]]] = []
|
||||
|
||||
steps.append(("Creating Django project", self.project_manager.create_project))
|
||||
|
||||
if self.metadata.get("predefined_structure"):
|
||||
# Build custom tree and then inject apps into settings
|
||||
steps.append(("Creating predefined structure", self.file_manager.create_predefined_structure))
|
||||
steps.append(("Adding apps to settings", self.project_manager.add_apps_to_settings))
|
||||
else:
|
||||
steps.append(("Creating Django apps", self.project_manager.create_apps))
|
||||
steps.append(("Creating project URLs", self.file_manager.create_project_urls))
|
||||
|
||||
steps.extend(
|
||||
[
|
||||
("Validating project structure", self.project_manager.validate_project_structure),
|
||||
("Creating utility files", self._create_utility_files),
|
||||
("Creating Procfile", self.file_manager.create_procfile),
|
||||
("Creating Justfile", self.file_manager.create_justfile),
|
||||
("Creating runtime.txt", self.file_manager.create_runtime_txt),
|
||||
("Creating CI/CD pipelines", self._create_cicd_pipelines),
|
||||
]
|
||||
)
|
||||
|
||||
total_steps = len(steps)
|
||||
success = True
|
||||
|
||||
+1
-1
@@ -58,7 +58,7 @@ def main() -> None:
|
||||
django_cli = Cli(project_dir, project_name, primary_app, app_names, metadata)
|
||||
success = django_cli.run_setup()
|
||||
|
||||
UIFormatter.create_summary_panel(project_dir, project_name, app_names, True)
|
||||
UIFormatter.create_summary_panel(project_dir, project_name, app_names, True, metadata=metadata)
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ from djinit.utils import (
|
||||
insert_apps_into_user_defined_apps,
|
||||
is_django_project,
|
||||
)
|
||||
from djinit.scripts.template_engine import template_engine
|
||||
from djinit.utils import create_file_with_content, create_init_file
|
||||
|
||||
|
||||
class AppManager:
|
||||
@@ -63,6 +65,11 @@ class AppManager:
|
||||
UIFormatter.print_error("Could not find manage.py file in project root")
|
||||
return False
|
||||
|
||||
# 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"))
|
||||
|
||||
# Otherwise, use standard flat app generation
|
||||
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}")
|
||||
@@ -118,3 +125,96 @@ class AppManager:
|
||||
|
||||
# Detect nested structure from settings file
|
||||
return detect_nested_structure_from_settings(settings_base_path, self.current_dir)
|
||||
|
||||
def _is_predefined_structure(self) -> bool:
|
||||
# Heuristic: presence of 'apps' directory at project root (where manage.py lives) and 'api' dir
|
||||
apps_dir = os.path.join(self.current_dir, "apps")
|
||||
api_dir = os.path.join(self.current_dir, "api")
|
||||
return os.path.isdir(apps_dir) and os.path.isdir(api_dir)
|
||||
|
||||
def _create_predefined_app(self, apps_dir: str) -> bool:
|
||||
"""Create an app following the predefined nested structure with custom templates."""
|
||||
app_dir = os.path.join(apps_dir, self.app_name)
|
||||
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("app/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}"
|
||||
|
||||
# Subfolders
|
||||
subfolders = {
|
||||
"models": [(f"{self.app_name}.py", "custom/app/models_generic.j2")],
|
||||
"serializers": [(f"{self.app_name}_serializer.py", "custom/app/serializers_generic.j2")],
|
||||
"services": [(f"{self.app_name}_service.py", "custom/app/services_generic.j2")],
|
||||
"views": [(f"{self.app_name}_view.py", "custom/app/views_generic.j2")],
|
||||
"tests": [(f"test_{self.app_name}_api.py", "custom/app/tests_generic.j2")],
|
||||
}
|
||||
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}",
|
||||
)
|
||||
|
||||
# urls.py at app root
|
||||
urls_content = template_engine.render_template(
|
||||
"custom/app/urls_generic.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")
|
||||
|
||||
# Add route include to api/v1/urls.py if present
|
||||
self._add_to_api_v1_urls(self.app_name)
|
||||
|
||||
UIFormatter.print_success(f"Created Django app '{self.app_name}' with predefined structure in {apps_dir}")
|
||||
return True
|
||||
|
||||
def _add_to_api_v1_urls(self, app_name: str) -> None:
|
||||
api_v1_urls = os.path.join(self.current_dir, "api", "v1", "urls.py")
|
||||
if not os.path.exists(api_v1_urls):
|
||||
return
|
||||
try:
|
||||
with open(api_v1_urls, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
import_line = "from django.urls import include, path"
|
||||
if import_line not in content:
|
||||
# ensure base import exists (fallback)
|
||||
content = import_line + "\n\n" + content
|
||||
|
||||
include_stmt = f"path(\"{app_name}/\", include(\"apps.{app_name}.urls\")),"
|
||||
if include_stmt in content:
|
||||
# already present
|
||||
return
|
||||
|
||||
# Insert into urlpatterns list before closing ]
|
||||
if "urlpatterns = [" in content:
|
||||
parts = content.split("urlpatterns = [", 1)
|
||||
before = parts[0]
|
||||
rest = parts[1]
|
||||
if "]" in rest:
|
||||
idx = rest.rfind("]")
|
||||
new_rest = rest[:idx]
|
||||
# ensure newline and indentation
|
||||
if not new_rest.endswith("\n"):
|
||||
new_rest += "\n"
|
||||
new_rest += f" {include_stmt}\n" + rest[idx:]
|
||||
content = before + "urlpatterns = [" + new_rest
|
||||
|
||||
with open(api_v1_urls, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
except Exception:
|
||||
# Non-fatal; leave API routes unchanged if edit fails
|
||||
return
|
||||
@@ -234,6 +234,7 @@ class UIFormatter:
|
||||
app_names: List[str],
|
||||
success: bool,
|
||||
duration: Optional[float] = None,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
"""Create a clean completion summary with optional duration"""
|
||||
console.print("\n")
|
||||
@@ -253,7 +254,18 @@ class UIFormatter:
|
||||
console.print(" [dim]│[/dim]")
|
||||
console.print(f" [dim]├─[/dim] [bold]Directory:[/bold] [white]{project_dir}[/white]")
|
||||
console.print(f" [dim]├─[/dim] [bold]Project:[/bold] [white]{project_name}[/white]")
|
||||
console.print(f" [dim]└─[/dim] [bold]Apps:[/bold] [white]{', '.join(app_names)}[/white]")
|
||||
# Predefined structure adjustments
|
||||
if metadata and metadata.get("predefined_structure"):
|
||||
module_name = metadata.get("project_module_name") or project_name
|
||||
console.print(f" [dim]├─[/dim] [bold]Module:[/bold] [white]{module_name}[/white]")
|
||||
console.print(f" [dim]├─[/dim] [bold]Structure:[/bold] [white]Predefined[/white]")
|
||||
# Hide empty apps list in predefined quick flow
|
||||
if app_names:
|
||||
console.print(f" [dim]└─[/dim] [bold]Apps:[/bold] [white]{', '.join(app_names)}[/white]")
|
||||
else:
|
||||
console.print(f" [dim]└─[/dim] [bold]Apps:[/bold] [white]users, core[/white]")
|
||||
else:
|
||||
console.print(f" [dim]└─[/dim] [bold]Apps:[/bold] [white]{', '.join(app_names)}[/white]")
|
||||
console.print()
|
||||
|
||||
# Next steps
|
||||
|
||||
+123
-4
@@ -10,6 +10,7 @@ from djinit.utils import (
|
||||
calculate_app_module_paths,
|
||||
change_cwd,
|
||||
create_file_with_content,
|
||||
create_init_file,
|
||||
)
|
||||
|
||||
|
||||
@@ -19,7 +20,10 @@ class FileManager:
|
||||
self.project_name = project_name
|
||||
self.app_names = app_names
|
||||
self.metadata = metadata
|
||||
self.project_configs = os.path.join(project_root, project_name)
|
||||
# Use module name (e.g., 'config') for Django package paths if provided
|
||||
# Fall back to project_name if key present but None
|
||||
self.module_name = metadata.get("project_module_name") or project_name
|
||||
self.project_configs = os.path.join(project_root, self.module_name)
|
||||
self.settings_folder = os.path.join(self.project_configs, "settings")
|
||||
self.settings_file = os.path.join(self.project_configs, "settings.py")
|
||||
|
||||
@@ -44,7 +48,12 @@ class FileManager:
|
||||
|
||||
def create_readme(self) -> bool:
|
||||
with change_cwd(self.project_root):
|
||||
context = {"project_name": self.project_name, "app_names": self.app_names}
|
||||
context = {
|
||||
"project_name": self.project_name,
|
||||
"app_names": self.app_names,
|
||||
"predefined_structure": bool(self.metadata.get("predefined_structure")),
|
||||
"module_name": self.module_name,
|
||||
}
|
||||
readme_content = template_engine.render_template("shared/readme.j2", context)
|
||||
return create_file_with_content(
|
||||
"README.md",
|
||||
@@ -104,6 +113,116 @@ class FileManager:
|
||||
)
|
||||
return True
|
||||
|
||||
def create_predefined_structure(self) -> bool:
|
||||
"""Create the predefined project structure under apps/ and api/ using custom templates.
|
||||
|
||||
Expected layout:
|
||||
- apps/users with nested folders (models, serializers, services, views, tests)
|
||||
- apps/core with utils, mixins, exceptions, middleware
|
||||
- api/ with versioned urls
|
||||
- overwrite project urls to include api urls
|
||||
"""
|
||||
# Ensure apps package
|
||||
apps_dir = os.path.join(self.project_root, "apps")
|
||||
os.makedirs(apps_dir, exist_ok=True)
|
||||
create_init_file(apps_dir, "Created apps/__init__.py")
|
||||
|
||||
# users app
|
||||
users_dir = os.path.join(apps_dir, "users")
|
||||
os.makedirs(users_dir, exist_ok=True)
|
||||
create_init_file(users_dir, "Created apps/users/__init__.py")
|
||||
|
||||
# apps.py can reuse standard app template
|
||||
users_context = {"app_name": "users"}
|
||||
users_apps_py = template_engine.render_template("app/apps.j2", users_context)
|
||||
create_file_with_content(os.path.join(users_dir, "apps.py"), users_apps_py, "Created apps/users/apps.py")
|
||||
|
||||
# Nested folders and files for users
|
||||
users_subfolders = {
|
||||
"models": [
|
||||
("user.py", "custom/users/models_user.j2"),
|
||||
],
|
||||
"serializers": [
|
||||
("user_serializer.py", "custom/users/serializers_user.j2"),
|
||||
],
|
||||
"services": [
|
||||
("user_service.py", "custom/users/services_user.j2"),
|
||||
],
|
||||
"views": [
|
||||
("user_view.py", "custom/users/views_user.j2"),
|
||||
],
|
||||
"tests": [
|
||||
("test_user_api.py", "custom/users/tests_user_api.j2"),
|
||||
],
|
||||
}
|
||||
for folder, files in users_subfolders.items():
|
||||
folder_path = os.path.join(users_dir, folder)
|
||||
os.makedirs(folder_path, exist_ok=True)
|
||||
create_init_file(folder_path, f"Created apps/users/{folder}/__init__.py")
|
||||
for filename, tpl in files:
|
||||
content = template_engine.render_template(tpl, {"app_module": "apps.users"})
|
||||
create_file_with_content(
|
||||
os.path.join(folder_path, filename),
|
||||
content,
|
||||
f"Created apps/users/{folder}/{filename}",
|
||||
)
|
||||
|
||||
# users urls.py at app root
|
||||
users_urls_content = template_engine.render_template("custom/users/urls.j2", {"app_module": "apps.users"})
|
||||
create_file_with_content(os.path.join(users_dir, "urls.py"), users_urls_content, "Created apps/users/urls.py")
|
||||
|
||||
# core app
|
||||
core_dir = os.path.join(apps_dir, "core")
|
||||
os.makedirs(core_dir, exist_ok=True)
|
||||
create_init_file(core_dir, "Created apps/core/__init__.py")
|
||||
|
||||
# core subfolders and files
|
||||
core_subfolders = {
|
||||
"utils": [("responses.py", "custom/core/utils_responses.j2")],
|
||||
"mixins": [("timestamped_model.py", "custom/core/mixins_timestamped_model.j2")],
|
||||
"middleware": [("request_logger.py", "custom/core/middleware_request_logger.j2")],
|
||||
}
|
||||
for folder, files in core_subfolders.items():
|
||||
folder_path = os.path.join(core_dir, folder)
|
||||
os.makedirs(folder_path, exist_ok=True)
|
||||
create_init_file(folder_path, f"Created apps/core/{folder}/__init__.py")
|
||||
for filename, tpl in files:
|
||||
content = template_engine.render_template(tpl, {})
|
||||
create_file_with_content(
|
||||
os.path.join(folder_path, filename),
|
||||
content,
|
||||
f"Created apps/core/{folder}/{filename}",
|
||||
)
|
||||
|
||||
# core/exceptions.py
|
||||
exceptions_content = template_engine.render_template("custom/core/exceptions.j2", {})
|
||||
create_file_with_content(os.path.join(core_dir, "exceptions.py"), exceptions_content, "Created apps/core/exceptions.py")
|
||||
|
||||
# api package
|
||||
api_dir = os.path.join(self.project_root, "api")
|
||||
os.makedirs(api_dir, exist_ok=True)
|
||||
create_init_file(api_dir, "Created api/__init__.py")
|
||||
api_urls_content = template_engine.render_template("custom/api/urls.j2", {})
|
||||
create_file_with_content(os.path.join(api_dir, "urls.py"), api_urls_content, "Created api/urls.py")
|
||||
|
||||
# api/v1
|
||||
api_v1_dir = os.path.join(api_dir, "v1")
|
||||
os.makedirs(api_v1_dir, exist_ok=True)
|
||||
create_init_file(api_v1_dir, "Created api/v1/__init__.py")
|
||||
api_v1_urls_content = template_engine.render_template("custom/api/v1_urls.j2", {})
|
||||
create_file_with_content(os.path.join(api_v1_dir, "urls.py"), api_v1_urls_content, "Created api/v1/urls.py")
|
||||
|
||||
# overwrite project urls to include api
|
||||
project_urls_content = template_engine.render_template("custom/project_urls_api.j2", {"project_module": self.module_name})
|
||||
create_file_with_content(
|
||||
os.path.join(self.project_configs, "urls.py"),
|
||||
project_urls_content,
|
||||
"Updated project urls.py to include API routes",
|
||||
should_format=True,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
def create_procfile(self) -> bool:
|
||||
with change_cwd(self.project_root):
|
||||
context = {"project_name": self.project_name}
|
||||
@@ -131,7 +250,7 @@ class FileManager:
|
||||
github_dir = os.path.join(self.project_root, ".github", "workflows")
|
||||
os.makedirs(github_dir, exist_ok=True)
|
||||
|
||||
context = {"project_name": self.project_name}
|
||||
context = {"project_name": self.project_name, "module_name": self.module_name}
|
||||
workflow_content = template_engine.render_template("shared/github_actions_ci.j2", context)
|
||||
workflow_file = os.path.join(github_dir, "ci.yml")
|
||||
create_file_with_content(
|
||||
@@ -143,7 +262,7 @@ class FileManager:
|
||||
|
||||
def create_gitlab_ci(self) -> bool:
|
||||
with change_cwd(self.project_root):
|
||||
context = {"project_name": self.project_name}
|
||||
context = {"project_name": self.project_name, "module_name": self.module_name}
|
||||
gitlab_ci_content = template_engine.render_template("shared/gitlab_ci.j2", context)
|
||||
create_file_with_content(
|
||||
".gitlab-ci.yml",
|
||||
|
||||
@@ -34,6 +34,9 @@ class ProjectMetadata:
|
||||
nested_apps: bool = False
|
||||
nested_dir: str | None = None
|
||||
use_database_url: bool = False
|
||||
# predefined structure support
|
||||
predefined_structure: bool = False
|
||||
project_module_name: str | None = None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
@@ -43,6 +46,8 @@ class ProjectMetadata:
|
||||
"nested_apps": self.nested_apps,
|
||||
"nested_dir": self.nested_dir,
|
||||
"use_database_url": self.use_database_url,
|
||||
"predefined_structure": self.predefined_structure,
|
||||
"project_module_name": self.project_module_name,
|
||||
}
|
||||
|
||||
|
||||
@@ -380,7 +385,47 @@ def get_user_input() -> Tuple[str, str, str, list, dict]:
|
||||
collector = InputCollector()
|
||||
console.print()
|
||||
|
||||
# Section 1: Project Setup
|
||||
# Pre-step: Ask for predefined structure first
|
||||
UIFormatter.print_separator()
|
||||
console.print(f"\n[{UIColors.INFO}]Choose Structure[/{UIColors.INFO}]\n")
|
||||
use_predefined = CharReader.get_yes_no(
|
||||
f"[{UIColors.HIGHLIGHT}]Use predefined structure? (Y/n):[/{UIColors.HIGHLIGHT}]"
|
||||
)
|
||||
|
||||
if use_predefined == "y":
|
||||
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] = []
|
||||
|
||||
# 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()
|
||||
metadata = ProjectMetadata(
|
||||
package_name=project_dir,
|
||||
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",
|
||||
)
|
||||
|
||||
return project_dir, project_name, "", app_names, metadata.to_dict()
|
||||
|
||||
# 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}]")
|
||||
|
||||
+33
-24
@@ -23,11 +23,14 @@ class ProjectManager:
|
||||
self.app_names = app_names
|
||||
self.metadata = metadata
|
||||
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
|
||||
|
||||
def create_project(self) -> bool:
|
||||
os.makedirs(self.project_root, exist_ok=True)
|
||||
|
||||
success = DjangoHelper.startproject(self.project_name, self.project_root)
|
||||
success = DjangoHelper.startproject(self.module_name, self.project_root)
|
||||
if success:
|
||||
UIFormatter.print_success(f"Django project '{self.project_name}' created successfully!")
|
||||
else:
|
||||
@@ -61,13 +64,13 @@ class ProjectManager:
|
||||
|
||||
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.project_name)
|
||||
base_settings_path = get_base_settings_path(self.project_root, self.module_name)
|
||||
|
||||
if not os.path.exists(base_settings_path):
|
||||
UIFormatter.print_error("Could not find base.py settings file")
|
||||
return False
|
||||
|
||||
content = read_base_settings(self.project_root, self.project_name)
|
||||
content = read_base_settings(self.project_root, self.module_name)
|
||||
if content is None:
|
||||
UIFormatter.print_error("Could not read base.py settings file")
|
||||
return False
|
||||
@@ -92,36 +95,42 @@ 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"),
|
||||
os.path.join(self.project_root, self.project_name, "__init__.py"),
|
||||
os.path.join(self.project_root, self.project_name, "settings", "__init__.py"),
|
||||
os.path.join(self.project_root, self.project_name, "settings", "base.py"),
|
||||
os.path.join(self.project_root, self.project_name, "settings", "development.py"),
|
||||
os.path.join(self.project_root, self.project_name, "settings", "production.py"),
|
||||
os.path.join(self.project_root, self.project_name, "urls.py"),
|
||||
os.path.join(self.project_root, self.project_name, "wsgi.py"),
|
||||
os.path.join(self.project_root, self.project_name, "asgi.py"),
|
||||
os.path.join(self.project_root, self.module_name, "__init__.py"),
|
||||
os.path.join(self.project_root, self.module_name, "settings", "__init__.py"),
|
||||
os.path.join(self.project_root, self.module_name, "settings", "base.py"),
|
||||
os.path.join(self.project_root, self.module_name, "settings", "development.py"),
|
||||
os.path.join(self.project_root, self.module_name, "settings", "production.py"),
|
||||
os.path.join(self.project_root, self.module_name, "urls.py"),
|
||||
os.path.join(self.project_root, self.module_name, "wsgi.py"),
|
||||
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"))
|
||||
|
||||
for app_name in self.app_names:
|
||||
app_files = [
|
||||
os.path.join(apps_base_dir, app_name, "__init__.py"),
|
||||
os.path.join(apps_base_dir, app_name, "apps.py"),
|
||||
os.path.join(apps_base_dir, app_name, "models.py"),
|
||||
os.path.join(apps_base_dir, app_name, "views.py"),
|
||||
os.path.join(apps_base_dir, app_name, "serializers.py"),
|
||||
os.path.join(apps_base_dir, app_name, "routes.py"),
|
||||
os.path.join(apps_base_dir, app_name, "tests.py"),
|
||||
os.path.join(apps_base_dir, app_name, "migrations"),
|
||||
os.path.join(apps_base_dir, app_name, "admin.py"),
|
||||
]
|
||||
required_files.extend(app_files)
|
||||
# In predefined structure, apps layout differs; skip strict per-app validation
|
||||
if not self.metadata.get("predefined_structure"):
|
||||
for app_name in self.app_names:
|
||||
app_files = [
|
||||
os.path.join(apps_base_dir, app_name, "__init__.py"),
|
||||
os.path.join(apps_base_dir, app_name, "apps.py"),
|
||||
os.path.join(apps_base_dir, app_name, "models.py"),
|
||||
os.path.join(apps_base_dir, app_name, "views.py"),
|
||||
os.path.join(apps_base_dir, app_name, "serializers.py"),
|
||||
os.path.join(apps_base_dir, app_name, "routes.py"),
|
||||
os.path.join(apps_base_dir, app_name, "tests.py"),
|
||||
os.path.join(apps_base_dir, app_name, "migrations"),
|
||||
os.path.join(apps_base_dir, app_name, "admin.py"),
|
||||
]
|
||||
required_files.extend(app_files)
|
||||
|
||||
missing_files = []
|
||||
for file_path in required_files:
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
from django.urls import include, path
|
||||
|
||||
app_name = "api"
|
||||
|
||||
urlpatterns = [
|
||||
path("v1/", include("api.v1.urls")),
|
||||
]
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
from django.urls import include, path
|
||||
|
||||
app_name = "v1"
|
||||
|
||||
urlpatterns = [
|
||||
path("users/", include("apps.users.urls")),
|
||||
]
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
from django.db import models
|
||||
|
||||
|
||||
class {{ model_class_name }}(models.Model):
|
||||
"""Generated model for {{ app_name }} app."""
|
||||
|
||||
# Add fields here
|
||||
pass
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from {{ app_module }}.models.{{ app_name }} import {{ model_class_name }}
|
||||
|
||||
|
||||
class {{ model_class_name }}Serializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = {{ model_class_name }}
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
from typing import Optional
|
||||
|
||||
from {{ app_module }}.models.{{ app_name }} import {{ model_class_name }}
|
||||
|
||||
|
||||
def get_{{ app_name }}_by_id(object_id: int) -> Optional[{{ model_class_name }}]:
|
||||
try:
|
||||
return {{ model_class_name }}.objects.get(id=object_id)
|
||||
except {{ model_class_name }}.DoesNotExist:
|
||||
return None
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_{{ app_name }}_list_empty():
|
||||
client = APIClient()
|
||||
url = reverse("v1:{{ app_name }}-list")
|
||||
response = client.get(url)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == []
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
from django.urls import include, path
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
from {{ app_module }}.views.{{ app_name }}_view import {{ model_class_name }}ViewSet
|
||||
|
||||
|
||||
app_name = "{{ app_name }}"
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r"{{ app_name }}", {{ model_class_name }}ViewSet, basename="{{ app_name }}")
|
||||
|
||||
urlpatterns = [
|
||||
path("", include(router.urls)),
|
||||
]
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
from rest_framework import viewsets
|
||||
|
||||
from {{ app_module }}.models.{{ app_name }} import {{ model_class_name }}
|
||||
from {{ app_module }}.serializers.{{ app_name }}_serializer import {{ model_class_name }}Serializer
|
||||
|
||||
|
||||
class {{ model_class_name }}ViewSet(viewsets.ModelViewSet):
|
||||
queryset = {{ model_class_name }}.objects.all()
|
||||
serializer_class = {{ model_class_name }}Serializer
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
class ApplicationError(Exception):
|
||||
"""Base application exception."""
|
||||
|
||||
def __init__(self, message: str):
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import logging
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RequestLoggerMiddleware:
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
logger.info("%s %s", request.method, request.path)
|
||||
response = self.get_response(request)
|
||||
return response
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
from django.db import models
|
||||
|
||||
|
||||
class TimestampedModel(models.Model):
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
from rest_framework.response import Response
|
||||
|
||||
|
||||
def success(data=None, status_code=200):
|
||||
return Response({"success": True, "data": data}, status=status_code)
|
||||
|
||||
|
||||
def error(message, status_code=400):
|
||||
return Response({"success": False, "message": message}, status=status_code)
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
from django.contrib import admin
|
||||
from django.urls import include, path
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
path("api/", include("api.urls")),
|
||||
]
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.db import models
|
||||
|
||||
|
||||
class User(AbstractUser):
|
||||
"""Custom user model extending Django's AbstractUser."""
|
||||
|
||||
# Add custom fields here if needed
|
||||
pass
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from apps.users.models.user import User
|
||||
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ["id", "username", "email", "first_name", "last_name"]
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
from typing import Optional
|
||||
|
||||
from apps.users.models.user import User
|
||||
|
||||
|
||||
def get_user_by_id(user_id: int) -> Optional[User]:
|
||||
try:
|
||||
return User.objects.get(id=user_id)
|
||||
except User.DoesNotExist:
|
||||
return None
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_users_list_empty():
|
||||
client = APIClient()
|
||||
url = reverse("v1:users-list")
|
||||
response = client.get(url)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == []
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
from django.urls import include, path
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
from apps.users.views.user_view import UserViewSet
|
||||
|
||||
|
||||
app_name = "users"
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r"users", UserViewSet, basename="users")
|
||||
|
||||
urlpatterns = [
|
||||
path("", include(router.urls)),
|
||||
]
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
from rest_framework import status, viewsets
|
||||
from rest_framework.response import Response
|
||||
|
||||
from apps.users.models.user import User
|
||||
from apps.users.serializers.user_serializer import UserSerializer
|
||||
|
||||
|
||||
class UserViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
queryset = User.objects.all()
|
||||
serializer_class = UserSerializer
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
return super().list(request, *args, **kwargs)
|
||||
|
||||
|
||||
@@ -37,6 +37,8 @@ EMAIL_PORT = int(os.getenv('EMAIL_PORT', 587))
|
||||
EMAIL_USE_TLS = os.getenv('EMAIL_USE_TLS', 'True').lower() == 'true'
|
||||
EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER')
|
||||
EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD')
|
||||
DEFAULT_FROM_EMAIL = os.environ.get("EMAIL_HOST_USER")
|
||||
SERVER_EMAIL = os.environ.get("EMAIL_HOST_USER")
|
||||
|
||||
# Security settings for production
|
||||
SECURE_SSL_REDIRECT = True
|
||||
|
||||
@@ -21,3 +21,5 @@ EMAIL_PORT=
|
||||
EMAIL_USE_TLS=
|
||||
EMAIL_HOST_USER=
|
||||
EMAIL_HOST_PASSWORD=
|
||||
DEFAULT_FROM_EMAIL=
|
||||
SERVER_EMAIL=
|
||||
@@ -31,10 +31,10 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
{% raw %}- name: Set up Python ${{ matrix.python-version }}{% endraw %}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
{% raw %}python-version: ${{ matrix.python-version }}{% endraw %}
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
|
||||
- name: Run Django tests
|
||||
env:
|
||||
DJANGO_SETTINGS_MODULE: {{ project_name }}.settings.development
|
||||
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
DB_NAME: test_db
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
|
||||
- name: Check migrations
|
||||
env:
|
||||
DJANGO_SETTINGS_MODULE: {{ project_name }}.settings.development
|
||||
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
DB_NAME: test_db
|
||||
|
||||
@@ -36,7 +36,7 @@ test:
|
||||
stage: test
|
||||
image: python:3.13
|
||||
variables:
|
||||
DJANGO_SETTINGS_MODULE: {{ project_name }}.settings.development
|
||||
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
script:
|
||||
@@ -52,7 +52,7 @@ test:python310:
|
||||
stage: test
|
||||
image: python:3.10
|
||||
variables:
|
||||
DJANGO_SETTINGS_MODULE: {{ project_name }}.settings.development
|
||||
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
script:
|
||||
@@ -69,7 +69,7 @@ test:python311:
|
||||
stage: test
|
||||
image: python:3.11
|
||||
variables:
|
||||
DJANGO_SETTINGS_MODULE: {{ project_name }}.settings.development
|
||||
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
script:
|
||||
@@ -86,7 +86,7 @@ test:python312:
|
||||
stage: test
|
||||
image: python:3.12
|
||||
variables:
|
||||
DJANGO_SETTINGS_MODULE: {{ project_name }}.settings.development
|
||||
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
script:
|
||||
@@ -103,7 +103,7 @@ test:python313:
|
||||
stage: test
|
||||
image: python:3.13
|
||||
variables:
|
||||
DJANGO_SETTINGS_MODULE: {{ project_name }}.settings.development
|
||||
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
script:
|
||||
|
||||
@@ -36,6 +36,53 @@ A Django project created with django-setup.
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
{% if predefined_structure %}
|
||||
{{ project_name }}/
|
||||
├── {{ module_name }}/
|
||||
│ ├── settings/
|
||||
│ │ ├── base.py
|
||||
│ │ ├── development.py
|
||||
│ │ └── production.py
|
||||
│ ├── urls.py
|
||||
│ ├── wsgi.py
|
||||
│ └── asgi.py
|
||||
├── apps/
|
||||
│ ├── users/
|
||||
│ │ ├── apps.py
|
||||
│ │ ├── models/
|
||||
│ │ │ └── user.py
|
||||
│ │ ├── serializers/
|
||||
│ │ │ └── user_serializer.py
|
||||
│ │ ├── services/
|
||||
│ │ │ └── user_service.py
|
||||
│ │ ├── views/
|
||||
│ │ │ └── user_view.py
|
||||
│ │ ├── urls.py
|
||||
│ │ └── tests/
|
||||
│ │ └── test_user_api.py
|
||||
│ └── core/
|
||||
│ ├── utils/
|
||||
│ │ └── responses.py
|
||||
│ ├── mixins/
|
||||
│ │ └── timestamped_model.py
|
||||
│ ├── exceptions.py
|
||||
│ └── middleware/
|
||||
│ └── request_logger.py
|
||||
├── api/
|
||||
│ ├── __init__.py
|
||||
│ ├── urls.py
|
||||
│ └── v1/
|
||||
│ ├── __init__.py
|
||||
│ └── urls.py
|
||||
├── manage.py
|
||||
├── requirements.txt
|
||||
├── pyproject.toml
|
||||
├── .env.sample
|
||||
├── justfile
|
||||
├── Procfile
|
||||
├── runtime.txt
|
||||
└── README.md
|
||||
{% else %}
|
||||
{{ project_name }}/
|
||||
├── {{ project_name }}/
|
||||
│ ├── settings/
|
||||
@@ -51,6 +98,7 @@ A Django project created with django-setup.
|
||||
├── pyproject.toml
|
||||
├── .env.sample
|
||||
└── README.md
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
## API Documentation
|
||||
|
||||
Reference in new issue
Block a user