feat: add predefined project structure option with core and users apps

This commit is contained in:
sankalp committed 2025-11-07 10:48:01 +05:45
1 parent 5da43ab3a5
commit 105cd75595
31 files changed
+637 -51

No files matched your search

+35 -11
View File
@@ -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
View File
@@ -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)
+100
View File
@@ -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
+13 -1
View File
@@ -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
View File
@@ -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",
+46 -1
View File
@@ -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
View File
@@ -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:
+9
View File
@@ -0,0 +1,9 @@
from django.urls import include, path
app_name = "api"
urlpatterns = [
path("v1/", include("api.v1.urls")),
]
+9
View File
@@ -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() == []
+16
View File
@@ -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
+2
View File
@@ -21,3 +21,5 @@ EMAIL_PORT=
EMAIL_USE_TLS=
EMAIL_HOST_USER=
EMAIL_HOST_PASSWORD=
DEFAULT_FROM_EMAIL=
SERVER_EMAIL=
+4 -4
View File
@@ -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
+5 -5
View File
@@ -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:
+48
View File
@@ -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