From 33e87e7764ca3237f4f8875d863e844f98e70252 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sat, 8 Nov 2025 23:01:37 +0545 Subject: [PATCH] feat: add unified project structure option --- djinit/cli.py | 18 +- djinit/scripts/app_generator.py | 31 +- djinit/scripts/console_ui.py | 8 +- djinit/scripts/django_helper.py | 32 +- djinit/scripts/files.py | 445 ++++++++++-------- djinit/scripts/input_handler.py | 61 ++- djinit/scripts/project.py | 7 +- djinit/templates/{app => base}/admin.j2 | 0 djinit/templates/{app => base}/apps.j2 | 0 .../{app => base}/migrations_init.j2 | 0 djinit/templates/{app => base}/models.j2 | 0 djinit/templates/{app => base}/routes.j2 | 0 djinit/templates/{app => base}/serializers.j2 | 0 djinit/templates/{app => base}/tests.j2 | 0 djinit/templates/{app => base}/urls.j2 | 0 djinit/templates/{app => base}/views.j2 | 0 djinit/templates/custom/project_urls_api.j2 | 9 - .../{custom => predefined}/api/urls.j2 | 0 .../v1_urls.j2 => predefined/api/v1/urls.j2} | 0 .../apps/generic/models.j2} | 0 .../apps/generic/serializers.j2} | 0 .../apps/generic/services.j2} | 0 .../apps/generic/tests.j2} | 0 .../apps/generic/urls.j2} | 0 .../apps/generic/views.j2} | 0 .../apps/users/models.j2} | 0 .../apps/users/serializers.j2} | 0 .../apps/users/services.j2} | 0 .../apps/users/tests.j2} | 0 .../{custom => predefined/apps}/users/urls.j2 | 0 .../apps/users/views.j2} | 0 .../{custom => predefined}/core/exceptions.j2 | 0 .../core/middleware/request_logger.j2} | 0 .../core/mixins/timestamped_model.j2} | 0 .../core/utils/responses.j2} | 0 .../{settings_base.j2 => settings/base.j2} | 0 .../development.j2} | 0 .../production.j2} | 0 djinit/templates/project/urls_with_api.j2 | 29 ++ .../{project_urls.j2 => urls_with_apps.j2} | 0 .../github_actions.j2} | 0 djinit/templates/shared/{ => ci}/gitlab_ci.j2 | 0 .../{project => shared}/manage_py.j2 | 0 djinit/templates/shared/readme.j2 | 40 ++ djinit/templates/unified/apps/core/apps.j2 | 9 + .../unified/apps/core/models/__init__.py.j2 | 5 + .../unified/apps/core/models/base.py.j2 | 14 + .../unified/apps/core/models/mixins.py.j2 | 14 + .../unified/apps/core/utils/__init__.py.j2 | 4 + .../unified/apps/core/utils/responses.py.j2 | 14 + yoo/.env.sample | 20 + yoo/.gitignore | 93 ++++ yoo/Procfile | 20 + yoo/README.md | 84 ++++ yoo/apps/__init__.py | 0 yoo/apps/api/__init__.py | 0 yoo/apps/api/admin/__init__.py | 0 yoo/apps/api/apps.py | 8 + yoo/apps/api/models/__init__.py | 0 yoo/apps/api/serializers/__init__.py | 0 yoo/apps/api/services/__init__.py | 0 yoo/apps/api/tests/__init__.py | 0 yoo/apps/api/urls/__init__.py | 0 yoo/apps/api/views/__init__.py | 0 yoo/apps/core/__init__.py | 0 yoo/apps/core/apps.py | 8 + yoo/apps/core/middleware/__init__.py | 0 yoo/apps/core/models/__init__.py | 5 + yoo/apps/core/models/base.py | 13 + yoo/apps/core/models/mixins.py | 13 + yoo/apps/core/permissions/__init__.py | 0 yoo/apps/core/utils/__init__.py | 3 + yoo/apps/core/utils/responses.py | 13 + yoo/core/__init__.py | 0 yoo/core/asgi.py | 11 + yoo/core/settings/__init__.py | 0 yoo/core/settings/base.py | 163 +++++++ yoo/core/settings/development.py | 29 ++ yoo/core/settings/production.py | 47 ++ yoo/core/urls.py | 30 ++ yoo/core/wsgi.py | 11 + yoo/justfile | 154 ++++++ yoo/manage.py | 29 ++ yoo/pyproject.toml | 41 ++ yoo/requirements.txt | 14 + yoo/runtime.txt | 1 + 86 files changed, 1309 insertions(+), 241 deletions(-) rename djinit/templates/{app => base}/admin.j2 (100%) rename djinit/templates/{app => base}/apps.j2 (100%) rename djinit/templates/{app => base}/migrations_init.j2 (100%) rename djinit/templates/{app => base}/models.j2 (100%) rename djinit/templates/{app => base}/routes.j2 (100%) rename djinit/templates/{app => base}/serializers.j2 (100%) rename djinit/templates/{app => base}/tests.j2 (100%) rename djinit/templates/{app => base}/urls.j2 (100%) rename djinit/templates/{app => base}/views.j2 (100%) delete mode 100644 djinit/templates/custom/project_urls_api.j2 rename djinit/templates/{custom => predefined}/api/urls.j2 (100%) rename djinit/templates/{custom/api/v1_urls.j2 => predefined/api/v1/urls.j2} (100%) rename djinit/templates/{custom/app/models_generic.j2 => predefined/apps/generic/models.j2} (100%) rename djinit/templates/{custom/app/serializers_generic.j2 => predefined/apps/generic/serializers.j2} (100%) rename djinit/templates/{custom/app/services_generic.j2 => predefined/apps/generic/services.j2} (100%) rename djinit/templates/{custom/app/tests_generic.j2 => predefined/apps/generic/tests.j2} (100%) rename djinit/templates/{custom/app/urls_generic.j2 => predefined/apps/generic/urls.j2} (100%) rename djinit/templates/{custom/app/views_generic.j2 => predefined/apps/generic/views.j2} (100%) rename djinit/templates/{custom/users/models_user.j2 => predefined/apps/users/models.j2} (100%) rename djinit/templates/{custom/users/serializers_user.j2 => predefined/apps/users/serializers.j2} (100%) rename djinit/templates/{custom/users/services_user.j2 => predefined/apps/users/services.j2} (100%) rename djinit/templates/{custom/users/tests_user_api.j2 => predefined/apps/users/tests.j2} (100%) rename djinit/templates/{custom => predefined/apps}/users/urls.j2 (100%) rename djinit/templates/{custom/users/views_user.j2 => predefined/apps/users/views.j2} (100%) rename djinit/templates/{custom => predefined}/core/exceptions.j2 (100%) rename djinit/templates/{custom/core/middleware_request_logger.j2 => predefined/core/middleware/request_logger.j2} (100%) rename djinit/templates/{custom/core/mixins_timestamped_model.j2 => predefined/core/mixins/timestamped_model.j2} (100%) rename djinit/templates/{custom/core/utils_responses.j2 => predefined/core/utils/responses.j2} (100%) rename djinit/templates/project/{settings_base.j2 => settings/base.j2} (100%) rename djinit/templates/project/{settings_development.j2 => settings/development.j2} (100%) rename djinit/templates/project/{settings_production.j2 => settings/production.j2} (100%) create mode 100644 djinit/templates/project/urls_with_api.j2 rename djinit/templates/project/{project_urls.j2 => urls_with_apps.j2} (100%) rename djinit/templates/shared/{github_actions_ci.j2 => ci/github_actions.j2} (100%) rename djinit/templates/shared/{ => ci}/gitlab_ci.j2 (100%) rename djinit/templates/{project => shared}/manage_py.j2 (100%) create mode 100644 djinit/templates/unified/apps/core/apps.j2 create mode 100644 djinit/templates/unified/apps/core/models/__init__.py.j2 create mode 100644 djinit/templates/unified/apps/core/models/base.py.j2 create mode 100644 djinit/templates/unified/apps/core/models/mixins.py.j2 create mode 100644 djinit/templates/unified/apps/core/utils/__init__.py.j2 create mode 100644 djinit/templates/unified/apps/core/utils/responses.py.j2 create mode 100644 yoo/.env.sample create mode 100644 yoo/.gitignore create mode 100644 yoo/Procfile create mode 100644 yoo/README.md create mode 100644 yoo/apps/__init__.py create mode 100644 yoo/apps/api/__init__.py create mode 100644 yoo/apps/api/admin/__init__.py create mode 100644 yoo/apps/api/apps.py create mode 100644 yoo/apps/api/models/__init__.py create mode 100644 yoo/apps/api/serializers/__init__.py create mode 100644 yoo/apps/api/services/__init__.py create mode 100644 yoo/apps/api/tests/__init__.py create mode 100644 yoo/apps/api/urls/__init__.py create mode 100644 yoo/apps/api/views/__init__.py create mode 100644 yoo/apps/core/__init__.py create mode 100644 yoo/apps/core/apps.py create mode 100644 yoo/apps/core/middleware/__init__.py create mode 100644 yoo/apps/core/models/__init__.py create mode 100644 yoo/apps/core/models/base.py create mode 100644 yoo/apps/core/models/mixins.py create mode 100644 yoo/apps/core/permissions/__init__.py create mode 100644 yoo/apps/core/utils/__init__.py create mode 100644 yoo/apps/core/utils/responses.py create mode 100644 yoo/core/__init__.py create mode 100644 yoo/core/asgi.py create mode 100644 yoo/core/settings/__init__.py create mode 100644 yoo/core/settings/base.py create mode 100644 yoo/core/settings/development.py create mode 100644 yoo/core/settings/production.py create mode 100644 yoo/core/urls.py create mode 100644 yoo/core/wsgi.py create mode 100644 yoo/justfile create mode 100755 yoo/manage.py create mode 100644 yoo/pyproject.toml create mode 100644 yoo/requirements.txt create mode 100644 yoo/runtime.txt diff --git a/djinit/cli.py b/djinit/cli.py index a706956..53dc429 100644 --- a/djinit/cli.py +++ b/djinit/cli.py @@ -41,11 +41,27 @@ class Cli: # propagate to managers self.project_manager.app_names = self.app_names + # If unified structure is enabled, adjust metadata defaults + if self.metadata.get("unified_structure"): + # For unified structure, module name is 'core' + self.metadata.setdefault("project_module_name", "core") + # Ensure nested apps live under 'apps' + self.metadata.setdefault("nested_apps", True) + self.metadata.setdefault("nested_dir", "apps") + # Default apps for unified structure (empty, apps/core and apps/api are created by default) + if not self.app_names: + self.app_names = [] + # 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"): + if self.metadata.get("unified_structure"): + # Build unified structure + steps.append(("Creating unified structure", self.file_manager.create_unified_structure)) + elif self.metadata.get("predefined_structure"): # Build custom tree and then inject apps into settings steps.append(("Creating predefined structure", self.file_manager.create_predefined_structure)) steps.append(("Adding apps to settings", self.project_manager.add_apps_to_settings)) diff --git a/djinit/scripts/app_generator.py b/djinit/scripts/app_generator.py index 9500aef..b594e8f 100644 --- a/djinit/scripts/app_generator.py +++ b/djinit/scripts/app_generator.py @@ -8,8 +8,11 @@ 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_init_file, detect_nested_structure_from_settings, extract_existing_apps, find_project_dir, @@ -17,8 +20,6 @@ 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: @@ -147,8 +148,10 @@ class AppManager: 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") + 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("_")]) @@ -156,11 +159,11 @@ class AppManager: # 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")], + "models": [(f"{self.app_name}.py", "predefined/apps/generic/models.j2")], + "serializers": [(f"{self.app_name}_serializer.py", "predefined/apps/generic/serializers.j2")], + "services": [(f"{self.app_name}_service.py", "predefined/apps/generic/services.j2")], + "views": [(f"{self.app_name}_view.py", "predefined/apps/generic/views.j2")], + "tests": [(f"test_{self.app_name}_api.py", "predefined/apps/generic/tests.j2")], } for folder, files in subfolders.items(): folder_path = os.path.join(app_dir, folder) @@ -179,9 +182,11 @@ class AppManager: # 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} + "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_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) @@ -194,7 +199,7 @@ class AppManager: if not os.path.exists(api_v1_urls): return try: - with open(api_v1_urls, "r", encoding="utf-8") as f: + with open(api_v1_urls, encoding="utf-8") as f: content = f.read() import_line = "from django.urls import include, path" @@ -202,7 +207,7 @@ class AppManager: # ensure base import exists (fallback) content = import_line + "\n\n" + content - include_stmt = f"path(\"{app_name}/\", include(\"apps.{app_name}.urls\"))," + include_stmt = f'path("{app_name}/", include("apps.{app_name}.urls")),' if include_stmt in content: # already present return diff --git a/djinit/scripts/console_ui.py b/djinit/scripts/console_ui.py index da60336..06aac3b 100644 --- a/djinit/scripts/console_ui.py +++ b/djinit/scripts/console_ui.py @@ -258,12 +258,12 @@ class UIFormatter: 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]") + console.print(" [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]") + console.print(" [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() @@ -274,9 +274,7 @@ class UIFormatter: console.print(" [dim]├─[/dim] [dim]1.[/dim] Navigate to your project directory") console.print(" [dim]├─[/dim] [dim]2.[/dim] Set environment variables in .env file") console.print(" [dim]├─[/dim] [dim]3.[/dim] Run migrations: [cyan]just migrate[/cyan]") - console.print( - " [dim]├─[/dim] [dim]4.[/dim] Create superuser: [cyan]just createsuperuser[/cyan]" - ) + console.print(" [dim]├─[/dim] [dim]4.[/dim] Create superuser: [cyan]just createsuperuser[/cyan]") console.print(" [dim]└─[/dim] [dim]5.[/dim] Start server: [cyan]just dev[/cyan]") console.print() diff --git a/djinit/scripts/django_helper.py b/djinit/scripts/django_helper.py index c28d781..b20fc82 100644 --- a/djinit/scripts/django_helper.py +++ b/djinit/scripts/django_helper.py @@ -15,15 +15,21 @@ class DjangoHelper: DJANGO_VERSION = "5.2" @staticmethod - def startproject(project_name: str, directory: str) -> bool: + def startproject(project_name: str, directory: str, unified: bool = False) -> bool: try: os.makedirs(directory, exist_ok=True) # Create manage.py manage_py_path = os.path.join(directory, "manage.py") - create_file_from_template(manage_py_path, "project/manage_py.j2", {}, "Created manage.py") + create_file_from_template(manage_py_path, "shared/manage_py.j2", {}, "Created manage.py") os.chmod(manage_py_path, 0o755) + # For unified structure, the project config is "core", not project_name + # The unified structure is created separately by create_unified_structure + if unified: + # Just create manage.py for unified, structure is created by create_unified_structure + return True + # Create project config directory project_config_dir = os.path.join(directory, project_name) os.makedirs(project_config_dir, exist_ok=True) @@ -37,19 +43,19 @@ class DjangoHelper: base_context = {"project_name": project_name, "app_names": []} create_file_from_template( os.path.join(settings_dir, "base.py"), - "project/settings_base.j2", + "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", + "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", + "project/settings/production.j2", {}, f"Created {project_name}/settings/production.py", ) @@ -93,14 +99,14 @@ class DjangoHelper: create_init_file(app_dir, f"Created {app_name}/__init__.py") app_files = [ - ("apps.py", "app/apps.j2"), - ("models.py", "app/models.j2"), - ("views.py", "app/views.j2"), - ("admin.py", "app/admin.j2"), - ("urls.py", "app/urls.j2"), - ("serializers.py", "app/serializers.j2"), - ("routes.py", "app/routes.j2"), - ("tests.py", "app/tests.j2"), + ("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"), ] for filename, template_path in app_files: diff --git a/djinit/scripts/files.py b/djinit/scripts/files.py index 6dc3afe..313415a 100644 --- a/djinit/scripts/files.py +++ b/djinit/scripts/files.py @@ -27,250 +27,319 @@ class FileManager: self.settings_folder = os.path.join(self.project_configs, "settings") self.settings_file = os.path.join(self.project_configs, "settings.py") + def _render_and_create_file( + self, + filepath: str, + template_path: str, + context: dict, + message: str, + base_dir: str = None, + should_format: bool = False, + ) -> bool: + """Helper to render template and create file.""" + target_dir = base_dir or self.project_root + with change_cwd(target_dir): + 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") + 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}") + def create_gitignore(self) -> bool: - with change_cwd(self.project_root): - gitignore_content = template_engine.render_template("shared/gitignore.j2", {}) - return create_file_with_content( - ".gitignore", - gitignore_content, - "Created .gitignore file", - ) + return self._render_and_create_file(".gitignore", "shared/gitignore.j2", {}, "Created .gitignore file") def create_requirements(self) -> bool: - with change_cwd(self.project_root): - context = {"use_database_url": self.metadata.get("use_database_url", True)} - requirements_content = template_engine.render_template("shared/requirements.j2", context) - return create_file_with_content( - "requirements.txt", - requirements_content, - "Created requirements.txt with Django dependencies", - ) + context = {"use_database_url": self.metadata.get("use_database_url", True)} + return self._render_and_create_file( + "requirements.txt", + "shared/requirements.j2", + context, + "Created requirements.txt with Django dependencies", + ) def create_readme(self) -> bool: - with change_cwd(self.project_root): - 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", - readme_content, - "Created README.md file", - ) + context = { + "project_name": self.project_name, + "app_names": self.app_names, + "predefined_structure": bool(self.metadata.get("predefined_structure")), + "unified_structure": bool(self.metadata.get("unified_structure")), + "module_name": self.module_name, + } + return self._render_and_create_file("README.md", "shared/readme.j2", context, "Created README.md file") def create_env_file(self) -> bool: """Create .env.sample file with environment variables.""" - with change_cwd(self.project_root): - context = { - "project_name": self.project_name, - "use_database_url": self.metadata.get("use_database_url", True), - } - env_content = template_engine.render_template("shared/env_sample.j2", context) - return create_file_with_content( - ".env.sample", - env_content, - "Created .env.sample file with generated secret key", - ) + context = { + "project_name": self.project_name, + "use_database_url": self.metadata.get("use_database_url", True), + } + return self._render_and_create_file( + ".env.sample", + "shared/env_sample.j2", + context, + "Created .env.sample file with generated secret key", + ) def create_pyproject_toml(self, metadata: dict) -> bool: - with change_cwd(self.project_root): - # 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" - context = {"package_name": package_name, "project_name": self.project_name} - pyproject_content = template_engine.render_template("shared/pyproject_toml.j2", context) - return create_file_with_content( - "pyproject.toml", - pyproject_content, - "Created pyproject.toml file for uv", - ) + # 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" + context = {"package_name": package_name, "project_name": self.project_name} + return self._render_and_create_file( + "pyproject.toml", + "shared/pyproject_toml.j2", + context, + "Created pyproject.toml file for uv", + ) def create_project_urls(self) -> bool: """Create project urls.py using project_urls.j2 template (replaces update_project_urls).""" - with change_cwd(self.project_configs): - effective_app_modules = calculate_app_module_paths(self.app_names, self.metadata) - context = { - "project_name": self.project_name, - "app_names": effective_app_modules, - } - urls_content = template_engine.render_template("project/project_urls.j2", context) - result = create_file_with_content( - "urls.py", - urls_content, - "Created project urls.py with comprehensive URL configuration", - should_format=True, - ) - return result + effective_app_modules = calculate_app_module_paths(self.app_names, self.metadata) + context = { + "project_name": self.project_name, + "app_names": effective_app_modules, + } + return self._render_and_create_file( + "urls.py", + "project/urls_with_apps.j2", + context, + "Created project urls.py with comprehensive URL configuration", + base_dir=self.project_configs, + should_format=True, + ) def create_justfile(self) -> bool: - with change_cwd(self.project_root): - context = {"project_name": self.project_name} - justfile_content = template_engine.render_template("shared/justfile.j2", context) - create_file_with_content( - "justfile", - justfile_content, - "Created justfile with Django development tasks", - ) - return True + context = {"project_name": self.project_name} + return self._render_and_create_file( + "justfile", + "shared/justfile.j2", + context, + "Created justfile with Django development tasks", + ) 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") + self._ensure_directory_with_init(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") + self._ensure_directory_with_init(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") + 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") # 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"), - ], + "models": [("user.py", "predefined/apps/users/models.j2")], + "serializers": [("user_serializer.py", "predefined/apps/users/serializers.j2")], + "services": [("user_service.py", "predefined/apps/users/services.j2")], + "views": [("user_view.py", "predefined/apps/users/views.j2")], + "tests": [("test_user_api.py", "predefined/apps/users/tests.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}", - ) + self._create_files_from_specs(users_dir, users_subfolders, {"app_module": "apps.users"}) # 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") + 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") # 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") + self._ensure_directory_with_init(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")], + "utils": [("responses.py", "predefined/core/utils/responses.j2")], + "mixins": [("timestamped_model.py", "predefined/core/mixins/timestamped_model.j2")], + "middleware": [("request_logger.py", "predefined/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}", - ) + self._create_files_from_specs(core_dir, core_subfolders, {}) # 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") + 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") # 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") + self._ensure_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") # 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") + self._ensure_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") # overwrite project urls to include api - project_urls_content = template_engine.render_template("custom/project_urls_api.j2", {"project_module": self.module_name}) + 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( - os.path.join(self.project_configs, "urls.py"), - project_urls_content, - "Updated project urls.py to include API routes", - should_format=True, + project_urls_path, 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} - procfile_content = template_engine.render_template("shared/procfile.j2", context) - create_file_with_content( - "Procfile", - procfile_content, - "Created Procfile with Gunicorn configuration", - ) + 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 core/settings directory + settings_dir = os.path.join(core_dir, "settings") + self._ensure_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 + + secret_key = generate_secret_key() + + # Create settings files using project templates with unified context + # For unified structure, project_module_name is "core" + base_context = { + "project_name": "core", # Use "core" as the module name + "app_names": ["apps.core", "apps.api"] + [f"apps.{app}" for app in self.app_names], + "use_database_url": self.metadata.get("use_database_url", True), + } + dev_context = {"secret_key": secret_key} + + # Settings files + for filename, context in [ + ("base.py", base_context), + ("development.py", dev_context), + ("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) + + # Core project files + core_files = [ + ("urls.py", "project/urls_with_api.j2", {"api_module": "apps.api"}), + ("wsgi.py", "project/wsgi.j2", {}), + ("asgi.py", "project/asgi.j2", {}), + ] + 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 apps directory + apps_dir = os.path.join(self.project_root, "apps") + self._ensure_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 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) + + # 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) + + # 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) + + # 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") + + # 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 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") + return True + def create_procfile(self) -> bool: + context = {"project_name": self.project_name} + return self._render_and_create_file( + "Procfile", + "shared/procfile.j2", + context, + "Created Procfile with Gunicorn configuration", + ) + def create_runtime_txt(self) -> bool: - with change_cwd(self.project_root): - context = {"python_version": "3.13"} - runtime_content = template_engine.render_template("shared/runtime_txt.j2", context) - create_file_with_content( - "runtime.txt", - runtime_content, - "Created runtime.txt with Python version specification", - ) - return True + context = {"python_version": "3.13"} + return self._render_and_create_file( + "runtime.txt", + "shared/runtime_txt.j2", + context, + "Created runtime.txt with Python version specification", + ) def create_github_actions(self) -> bool: - with change_cwd(self.project_root): - github_dir = os.path.join(self.project_root, ".github", "workflows") - os.makedirs(github_dir, exist_ok=True) - - 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( - workflow_file, - workflow_content, - "Created Github Actions workflow (ci.yml)", - ) - return True + github_dir = os.path.join(self.project_root, ".github", "workflows") + os.makedirs(github_dir, exist_ok=True) + context = {"project_name": self.project_name, "module_name": self.module_name} + workflow_file = os.path.join(github_dir, "ci.yml") + return self._render_and_create_file( + workflow_file, + "shared/ci/github_actions.j2", + context, + "Created Github Actions workflow (ci.yml)", + base_dir=self.project_root, + ) def create_gitlab_ci(self) -> bool: - with change_cwd(self.project_root): - 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", - gitlab_ci_content, - "Created GitLab CI configuration (.gitlab-ci.yml)", - ) - return True + context = {"project_name": self.project_name, "module_name": self.module_name} + return self._render_and_create_file( + ".gitlab-ci.yml", + "shared/ci/gitlab_ci.j2", + context, + "Created GitLab CI configuration (.gitlab-ci.yml)", + ) diff --git a/djinit/scripts/input_handler.py b/djinit/scripts/input_handler.py index c1502c4..65f4cc7 100644 --- a/djinit/scripts/input_handler.py +++ b/djinit/scripts/input_handler.py @@ -36,6 +36,7 @@ class ProjectMetadata: use_database_url: bool = False # predefined structure support predefined_structure: bool = False + unified_structure: bool = False project_module_name: str | None = None def to_dict(self) -> dict: @@ -47,6 +48,7 @@ class ProjectMetadata: "nested_dir": self.nested_dir, "use_database_url": self.use_database_url, "predefined_structure": self.predefined_structure, + "unified_structure": self.unified_structure, "project_module_name": self.project_module_name, } @@ -385,19 +387,27 @@ def get_user_input() -> Tuple[str, str, str, list, dict]: collector = InputCollector() console.print() - # Pre-step: Ask for predefined structure first + # Pre-step: Ask for structure type 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}]" - ) + 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() - if use_predefined == "y": + 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: 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}]" - ) + 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.") @@ -427,6 +437,39 @@ def get_user_input() -> Tuple[str, str, str, list, dict]: return project_dir, project_name, "", app_names, metadata.to_dict() + if 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) + + # 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 unified 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, + unified_structure=True, + project_module_name="core", + ) + + 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") diff --git a/djinit/scripts/project.py b/djinit/scripts/project.py index 29ba578..2105f02 100644 --- a/djinit/scripts/project.py +++ b/djinit/scripts/project.py @@ -30,7 +30,8 @@ class ProjectManager: def create_project(self) -> bool: os.makedirs(self.project_root, exist_ok=True) - success = DjangoHelper.startproject(self.module_name, self.project_root) + unified = self.metadata.get("unified_structure", False) + success = DjangoHelper.startproject(self.module_name, self.project_root, unified=unified) if success: UIFormatter.print_success(f"Django project '{self.project_name}' created successfully!") else: @@ -116,8 +117,8 @@ class ProjectManager: 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")) - # In predefined structure, apps layout differs; skip strict per-app validation - if not self.metadata.get("predefined_structure"): + # 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"): for app_name in self.app_names: app_files = [ os.path.join(apps_base_dir, app_name, "__init__.py"), diff --git a/djinit/templates/app/admin.j2 b/djinit/templates/base/admin.j2 similarity index 100% rename from djinit/templates/app/admin.j2 rename to djinit/templates/base/admin.j2 diff --git a/djinit/templates/app/apps.j2 b/djinit/templates/base/apps.j2 similarity index 100% rename from djinit/templates/app/apps.j2 rename to djinit/templates/base/apps.j2 diff --git a/djinit/templates/app/migrations_init.j2 b/djinit/templates/base/migrations_init.j2 similarity index 100% rename from djinit/templates/app/migrations_init.j2 rename to djinit/templates/base/migrations_init.j2 diff --git a/djinit/templates/app/models.j2 b/djinit/templates/base/models.j2 similarity index 100% rename from djinit/templates/app/models.j2 rename to djinit/templates/base/models.j2 diff --git a/djinit/templates/app/routes.j2 b/djinit/templates/base/routes.j2 similarity index 100% rename from djinit/templates/app/routes.j2 rename to djinit/templates/base/routes.j2 diff --git a/djinit/templates/app/serializers.j2 b/djinit/templates/base/serializers.j2 similarity index 100% rename from djinit/templates/app/serializers.j2 rename to djinit/templates/base/serializers.j2 diff --git a/djinit/templates/app/tests.j2 b/djinit/templates/base/tests.j2 similarity index 100% rename from djinit/templates/app/tests.j2 rename to djinit/templates/base/tests.j2 diff --git a/djinit/templates/app/urls.j2 b/djinit/templates/base/urls.j2 similarity index 100% rename from djinit/templates/app/urls.j2 rename to djinit/templates/base/urls.j2 diff --git a/djinit/templates/app/views.j2 b/djinit/templates/base/views.j2 similarity index 100% rename from djinit/templates/app/views.j2 rename to djinit/templates/base/views.j2 diff --git a/djinit/templates/custom/project_urls_api.j2 b/djinit/templates/custom/project_urls_api.j2 deleted file mode 100644 index 459de61..0000000 --- a/djinit/templates/custom/project_urls_api.j2 +++ /dev/null @@ -1,9 +0,0 @@ -from django.contrib import admin -from django.urls import include, path - -urlpatterns = [ - path("admin/", admin.site.urls), - path("api/", include("api.urls")), -] - - diff --git a/djinit/templates/custom/api/urls.j2 b/djinit/templates/predefined/api/urls.j2 similarity index 100% rename from djinit/templates/custom/api/urls.j2 rename to djinit/templates/predefined/api/urls.j2 diff --git a/djinit/templates/custom/api/v1_urls.j2 b/djinit/templates/predefined/api/v1/urls.j2 similarity index 100% rename from djinit/templates/custom/api/v1_urls.j2 rename to djinit/templates/predefined/api/v1/urls.j2 diff --git a/djinit/templates/custom/app/models_generic.j2 b/djinit/templates/predefined/apps/generic/models.j2 similarity index 100% rename from djinit/templates/custom/app/models_generic.j2 rename to djinit/templates/predefined/apps/generic/models.j2 diff --git a/djinit/templates/custom/app/serializers_generic.j2 b/djinit/templates/predefined/apps/generic/serializers.j2 similarity index 100% rename from djinit/templates/custom/app/serializers_generic.j2 rename to djinit/templates/predefined/apps/generic/serializers.j2 diff --git a/djinit/templates/custom/app/services_generic.j2 b/djinit/templates/predefined/apps/generic/services.j2 similarity index 100% rename from djinit/templates/custom/app/services_generic.j2 rename to djinit/templates/predefined/apps/generic/services.j2 diff --git a/djinit/templates/custom/app/tests_generic.j2 b/djinit/templates/predefined/apps/generic/tests.j2 similarity index 100% rename from djinit/templates/custom/app/tests_generic.j2 rename to djinit/templates/predefined/apps/generic/tests.j2 diff --git a/djinit/templates/custom/app/urls_generic.j2 b/djinit/templates/predefined/apps/generic/urls.j2 similarity index 100% rename from djinit/templates/custom/app/urls_generic.j2 rename to djinit/templates/predefined/apps/generic/urls.j2 diff --git a/djinit/templates/custom/app/views_generic.j2 b/djinit/templates/predefined/apps/generic/views.j2 similarity index 100% rename from djinit/templates/custom/app/views_generic.j2 rename to djinit/templates/predefined/apps/generic/views.j2 diff --git a/djinit/templates/custom/users/models_user.j2 b/djinit/templates/predefined/apps/users/models.j2 similarity index 100% rename from djinit/templates/custom/users/models_user.j2 rename to djinit/templates/predefined/apps/users/models.j2 diff --git a/djinit/templates/custom/users/serializers_user.j2 b/djinit/templates/predefined/apps/users/serializers.j2 similarity index 100% rename from djinit/templates/custom/users/serializers_user.j2 rename to djinit/templates/predefined/apps/users/serializers.j2 diff --git a/djinit/templates/custom/users/services_user.j2 b/djinit/templates/predefined/apps/users/services.j2 similarity index 100% rename from djinit/templates/custom/users/services_user.j2 rename to djinit/templates/predefined/apps/users/services.j2 diff --git a/djinit/templates/custom/users/tests_user_api.j2 b/djinit/templates/predefined/apps/users/tests.j2 similarity index 100% rename from djinit/templates/custom/users/tests_user_api.j2 rename to djinit/templates/predefined/apps/users/tests.j2 diff --git a/djinit/templates/custom/users/urls.j2 b/djinit/templates/predefined/apps/users/urls.j2 similarity index 100% rename from djinit/templates/custom/users/urls.j2 rename to djinit/templates/predefined/apps/users/urls.j2 diff --git a/djinit/templates/custom/users/views_user.j2 b/djinit/templates/predefined/apps/users/views.j2 similarity index 100% rename from djinit/templates/custom/users/views_user.j2 rename to djinit/templates/predefined/apps/users/views.j2 diff --git a/djinit/templates/custom/core/exceptions.j2 b/djinit/templates/predefined/core/exceptions.j2 similarity index 100% rename from djinit/templates/custom/core/exceptions.j2 rename to djinit/templates/predefined/core/exceptions.j2 diff --git a/djinit/templates/custom/core/middleware_request_logger.j2 b/djinit/templates/predefined/core/middleware/request_logger.j2 similarity index 100% rename from djinit/templates/custom/core/middleware_request_logger.j2 rename to djinit/templates/predefined/core/middleware/request_logger.j2 diff --git a/djinit/templates/custom/core/mixins_timestamped_model.j2 b/djinit/templates/predefined/core/mixins/timestamped_model.j2 similarity index 100% rename from djinit/templates/custom/core/mixins_timestamped_model.j2 rename to djinit/templates/predefined/core/mixins/timestamped_model.j2 diff --git a/djinit/templates/custom/core/utils_responses.j2 b/djinit/templates/predefined/core/utils/responses.j2 similarity index 100% rename from djinit/templates/custom/core/utils_responses.j2 rename to djinit/templates/predefined/core/utils/responses.j2 diff --git a/djinit/templates/project/settings_base.j2 b/djinit/templates/project/settings/base.j2 similarity index 100% rename from djinit/templates/project/settings_base.j2 rename to djinit/templates/project/settings/base.j2 diff --git a/djinit/templates/project/settings_development.j2 b/djinit/templates/project/settings/development.j2 similarity index 100% rename from djinit/templates/project/settings_development.j2 rename to djinit/templates/project/settings/development.j2 diff --git a/djinit/templates/project/settings_production.j2 b/djinit/templates/project/settings/production.j2 similarity index 100% rename from djinit/templates/project/settings_production.j2 rename to djinit/templates/project/settings/production.j2 diff --git a/djinit/templates/project/urls_with_api.j2 b/djinit/templates/project/urls_with_api.j2 new file mode 100644 index 0000000..e236407 --- /dev/null +++ b/djinit/templates/project/urls_with_api.j2 @@ -0,0 +1,29 @@ +"""generated with djinit""" + +from django.contrib import admin +from django.urls import path, include +from django.conf import settings +from drf_spectacular.views import ( + SpectacularAPIView, + SpectacularSwaggerView, +) +from rest_framework_simplejwt.views import ( + TokenObtainPairView, + TokenRefreshView, + TokenBlacklistView, +) + +urlpatterns = [ + path("admin/", admin.site.urls), + # JWT tokens + path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"), + path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), + path("token/blacklist/", TokenBlacklistView.as_view(), name="token_blacklist"), + path("api/", include("{{ api_module }}.urls")),] + +# Don't show schema in production +if settings.DEBUG: + urlpatterns += [ + path("schema/", SpectacularAPIView.as_view(), name="schema"), + path("docs/", SpectacularSwaggerView.as_view(url_name="schema")), + ] \ No newline at end of file diff --git a/djinit/templates/project/project_urls.j2 b/djinit/templates/project/urls_with_apps.j2 similarity index 100% rename from djinit/templates/project/project_urls.j2 rename to djinit/templates/project/urls_with_apps.j2 diff --git a/djinit/templates/shared/github_actions_ci.j2 b/djinit/templates/shared/ci/github_actions.j2 similarity index 100% rename from djinit/templates/shared/github_actions_ci.j2 rename to djinit/templates/shared/ci/github_actions.j2 diff --git a/djinit/templates/shared/gitlab_ci.j2 b/djinit/templates/shared/ci/gitlab_ci.j2 similarity index 100% rename from djinit/templates/shared/gitlab_ci.j2 rename to djinit/templates/shared/ci/gitlab_ci.j2 diff --git a/djinit/templates/project/manage_py.j2 b/djinit/templates/shared/manage_py.j2 similarity index 100% rename from djinit/templates/project/manage_py.j2 rename to djinit/templates/shared/manage_py.j2 diff --git a/djinit/templates/shared/readme.j2 b/djinit/templates/shared/readme.j2 index 50a5e31..0eb6b32 100644 --- a/djinit/templates/shared/readme.j2 +++ b/djinit/templates/shared/readme.j2 @@ -82,6 +82,46 @@ A Django project created with django-setup. ├── Procfile ├── runtime.txt └── README.md +{% elif unified_structure %} +{{ project_name }}/ +├── core/ +│ ├── settings/ +│ │ ├── base.py +│ │ ├── development.py +│ │ └── production.py +│ ├── urls.py +│ ├── wsgi.py +│ └── asgi.py +├── apps/ +│ ├── core/ +│ │ ├── apps.py +│ │ ├── models/ +│ │ │ ├── __init__.py +│ │ │ ├── base.py +│ │ │ └── mixins.py +│ │ ├── utils/ +│ │ │ ├── __init__.py +│ │ │ └── responses.py +│ │ ├── permissions/ +│ │ ├── middleware/ +│ │ └── __init__.py +│ └── api/ +│ ├── apps.py +│ ├── models/ +│ ├── serializers/ +│ ├── views/ +│ ├── services/ +│ ├── tests/ +│ ├── urls/ +│ └── admin/ +├── manage.py +├── requirements.txt +├── pyproject.toml +├── .env.sample +├── justfile +├── Procfile +├── runtime.txt +└── README.md {% else %} {{ project_name }}/ ├── {{ project_name }}/ diff --git a/djinit/templates/unified/apps/core/apps.j2 b/djinit/templates/unified/apps/core/apps.j2 new file mode 100644 index 0000000..e59efb0 --- /dev/null +++ b/djinit/templates/unified/apps/core/apps.j2 @@ -0,0 +1,9 @@ +"""generated with djinit""" + +from django.apps import AppConfig + + +class CoreConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "apps.core" + diff --git a/djinit/templates/unified/apps/core/models/__init__.py.j2 b/djinit/templates/unified/apps/core/models/__init__.py.j2 new file mode 100644 index 0000000..71067ec --- /dev/null +++ b/djinit/templates/unified/apps/core/models/__init__.py.j2 @@ -0,0 +1,5 @@ +from .base import AbstractBaseModel +from .mixins import TimestampedModel + +__all__ = ["AbstractBaseModel", "TimestampedModel"] + diff --git a/djinit/templates/unified/apps/core/models/base.py.j2 b/djinit/templates/unified/apps/core/models/base.py.j2 new file mode 100644 index 0000000..95ed131 --- /dev/null +++ b/djinit/templates/unified/apps/core/models/base.py.j2 @@ -0,0 +1,14 @@ +"""generated with djinit""" + +from django.db import models + + +class AbstractBaseModel(models.Model): + """Abstract base model with common fields.""" + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + abstract = True + diff --git a/djinit/templates/unified/apps/core/models/mixins.py.j2 b/djinit/templates/unified/apps/core/models/mixins.py.j2 new file mode 100644 index 0000000..c456739 --- /dev/null +++ b/djinit/templates/unified/apps/core/models/mixins.py.j2 @@ -0,0 +1,14 @@ +"""generated with djinit""" + +from django.db import models + + +class TimestampedModel(models.Model): + """Mixin for models that need timestamp fields.""" + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + abstract = True + diff --git a/djinit/templates/unified/apps/core/utils/__init__.py.j2 b/djinit/templates/unified/apps/core/utils/__init__.py.j2 new file mode 100644 index 0000000..56fdd17 --- /dev/null +++ b/djinit/templates/unified/apps/core/utils/__init__.py.j2 @@ -0,0 +1,4 @@ +from .responses import success, error + +__all__ = ["success", "error"] + diff --git a/djinit/templates/unified/apps/core/utils/responses.py.j2 b/djinit/templates/unified/apps/core/utils/responses.py.j2 new file mode 100644 index 0000000..7f32fda --- /dev/null +++ b/djinit/templates/unified/apps/core/utils/responses.py.j2 @@ -0,0 +1,14 @@ +"""generated with djinit""" + +from rest_framework.response import Response + + +def success(data=None, status_code=200): + """Return a successful response.""" + return Response({"success": True, "data": data}, status=status_code) + + +def error(message, status_code=400): + """Return an error response.""" + return Response({"success": False, "message": message}, status=status_code) + diff --git a/yoo/.env.sample b/yoo/.env.sample new file mode 100644 index 0000000..8130d94 --- /dev/null +++ b/yoo/.env.sample @@ -0,0 +1,20 @@ +# Django settings +DJANGO_SETTINGS_MODULE=yoo.settings.development +SECRET_KEY= your-secret-key +ALLOWED_HOSTS=api.your-domain.com,www.your-domain.com + +# Database (individual parameters) +DB_NAME= +DB_USER= +DB_PASSWORD= +DB_HOST= +DB_PORT= + +# Email settings +EMAIL_HOST= +EMAIL_PORT= +EMAIL_USE_TLS= +EMAIL_HOST_USER= +EMAIL_HOST_PASSWORD= +DEFAULT_FROM_EMAIL= +SERVER_EMAIL= diff --git a/yoo/.gitignore b/yoo/.gitignore new file mode 100644 index 0000000..2b5243c --- /dev/null +++ b/yoo/.gitignore @@ -0,0 +1,93 @@ +#--------------------------------------------------# +# The following was generated by djinit: # +#--------------------------------------------------# + +# Python +*.py[cod] +*$py.class +*.so +__pycache__/ +*.egg-info/ +dist/ +build/ +.eggs/ +*.egg + +# Django +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal +*.sqlite3 + +# Environment +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# IDEs +.vscode/ +.idea/ +*.swp +*.swo +*~ +.project +.pydevproject +.settings/ +.ropeproject/ + +# OS +.DS_Store +*.DS_Store +Thumbs.db +desktop.ini + +# Django specific +media/ +staticfiles/ +staticfiles/ +static_root/ +/static/ + +# Testing +.coverage +htmlcov/ +.pytest_cache/ +.tox/ +coverage.xml +*.cover +.hypothesis/ +.cache + +# Logs +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Node modules (if using frontend tools) +node_modules/ + +# Secret keys +secret.txt +secrets.json + +# Jupyter Notebook +.ipynb_checkpoints + +# Backup files +*.bak +*.tmp +*~ + +# Database +*.db +*.sql + +# Cloud +.cloud diff --git a/yoo/Procfile b/yoo/Procfile new file mode 100644 index 0000000..bffaa10 --- /dev/null +++ b/yoo/Procfile @@ -0,0 +1,20 @@ +# Procfile for yoo +# Used for deploying to PaaS platforms like Heroku, Railway, Render, etc. + +# Web server process (using Gunicorn) +# Adjust workers based on your needs: (2 x CPU cores) + 1 +web: gunicorn yoo.wsgi:application --bind 0.0.0.0:$PORT --workers 2 --timeout 120 --access-logfile - --error-logfile - --log-level info + +# Alternative web server configurations (uncomment to use): +# web: gunicorn yoo.wsgi:application --bind 0.0.0.0:$PORT --workers 4 --threads 2 --timeout 120 +# web: gunicorn yoo.wsgi:application --bind 0.0.0.0:$PORT --workers 2 --worker-class gevent --worker-connections 1000 --timeout 120 + +# Release task (runs migrations before deployment) +release: python manage.py migrate --noinput && python manage.py collectstatic --noinput + +# Uncomment below to run scheduled tasks (if using Celery) +# worker: celery -A yoo worker --loglevel=info +# beat: celery -A yoo beat --loglevel=info + +# Development server (for local testing with Foreman/Overmind) +# web: python manage.py runserver 0.0.0.0:$PORT diff --git a/yoo/README.md b/yoo/README.md new file mode 100644 index 0000000..6d4a03f --- /dev/null +++ b/yoo/README.md @@ -0,0 +1,84 @@ +# yoo + +A Django project created with django-setup. + +## Features + +- Modern project structure with environment-specific settings +- Pre-configured REST API with JWT authentication +- Essential dependencies and utilities +- Production-ready configuration + +## Setup + +1. Install dependencies: + ```bash + pip install -r requirements.txt + ``` + +2. Set environment variables in `.env` file + +3. Run migrations: + ```bash + python manage.py migrate + ``` + +4. Create superuser: + ```bash + python manage.py createsuperuser + ``` + +5. Start development server: + ```bash + python manage.py runserver + ``` + +## Project Structure + +``` +yoo/ +├── core/ +│ ├── settings/ +│ │ ├── base.py +│ │ ├── development.py +│ │ └── production.py +│ ├── urls.py +│ ├── wsgi.py +│ └── asgi.py +├── apps/ +│ ├── core/ +│ │ ├── apps.py +│ │ ├── models/ +│ │ │ ├── __init__.py +│ │ │ ├── base.py +│ │ │ └── mixins.py +│ │ ├── utils/ +│ │ │ ├── __init__.py +│ │ │ └── responses.py +│ │ ├── permissions/ +│ │ ├── middleware/ +│ │ └── __init__.py +│ └── api/ +│ ├── apps.py +│ ├── models/ +│ ├── serializers/ +│ ├── views/ +│ ├── services/ +│ ├── tests/ +│ ├── urls/ +│ └── admin/ +├── manage.py +├── requirements.txt +├── pyproject.toml +├── .env.sample +├── justfile +├── Procfile +├── runtime.txt +└── README.md +``` + +## API Documentation + +When running in development mode, API documentation is available at: +- Swagger UI: http://localhost:8000/docs/ +- Schema: http://localhost:8000/schema/ diff --git a/yoo/apps/__init__.py b/yoo/apps/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/yoo/apps/api/__init__.py b/yoo/apps/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/yoo/apps/api/admin/__init__.py b/yoo/apps/api/admin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/yoo/apps/api/apps.py b/yoo/apps/api/apps.py new file mode 100644 index 0000000..3ed621b --- /dev/null +++ b/yoo/apps/api/apps.py @@ -0,0 +1,8 @@ +"""generated with djinit""" + +from django.apps import AppConfig + + +class Apps.apiConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'apps.api' diff --git a/yoo/apps/api/models/__init__.py b/yoo/apps/api/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/yoo/apps/api/serializers/__init__.py b/yoo/apps/api/serializers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/yoo/apps/api/services/__init__.py b/yoo/apps/api/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/yoo/apps/api/tests/__init__.py b/yoo/apps/api/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/yoo/apps/api/urls/__init__.py b/yoo/apps/api/urls/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/yoo/apps/api/views/__init__.py b/yoo/apps/api/views/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/yoo/apps/core/__init__.py b/yoo/apps/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/yoo/apps/core/apps.py b/yoo/apps/core/apps.py new file mode 100644 index 0000000..00c1458 --- /dev/null +++ b/yoo/apps/core/apps.py @@ -0,0 +1,8 @@ +"""generated with djinit""" + +from django.apps import AppConfig + + +class Config(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "" diff --git a/yoo/apps/core/middleware/__init__.py b/yoo/apps/core/middleware/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/yoo/apps/core/models/__init__.py b/yoo/apps/core/models/__init__.py new file mode 100644 index 0000000..980c8e5 --- /dev/null +++ b/yoo/apps/core/models/__init__.py @@ -0,0 +1,5 @@ +"""generated with djinit""" + +from django.db import models + +# Create your models here. diff --git a/yoo/apps/core/models/base.py b/yoo/apps/core/models/base.py new file mode 100644 index 0000000..a3970f9 --- /dev/null +++ b/yoo/apps/core/models/base.py @@ -0,0 +1,13 @@ +"""generated with djinit""" + +from django.db import models + + +class AbstractBaseModel(models.Model): + """Abstract base model with common fields.""" + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + abstract = True diff --git a/yoo/apps/core/models/mixins.py b/yoo/apps/core/models/mixins.py new file mode 100644 index 0000000..fe40544 --- /dev/null +++ b/yoo/apps/core/models/mixins.py @@ -0,0 +1,13 @@ +"""generated with djinit""" + +from django.db import models + + +class TimestampedModel(models.Model): + """Mixin for models that need timestamp fields.""" + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + abstract = True diff --git a/yoo/apps/core/permissions/__init__.py b/yoo/apps/core/permissions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/yoo/apps/core/utils/__init__.py b/yoo/apps/core/utils/__init__.py new file mode 100644 index 0000000..155f5b4 --- /dev/null +++ b/yoo/apps/core/utils/__init__.py @@ -0,0 +1,3 @@ +from .responses import success, error + +__all__ = ["success", "error"] diff --git a/yoo/apps/core/utils/responses.py b/yoo/apps/core/utils/responses.py new file mode 100644 index 0000000..929c2f6 --- /dev/null +++ b/yoo/apps/core/utils/responses.py @@ -0,0 +1,13 @@ +"""generated with djinit""" + +from rest_framework.response import Response + + +def success(data=None, status_code=200): + """Return a successful response.""" + return Response({"success": True, "data": data}, status=status_code) + + +def error(message, status_code=400): + """Return an error response.""" + return Response({"success": False, "message": message}, status=status_code) diff --git a/yoo/core/__init__.py b/yoo/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/yoo/core/asgi.py b/yoo/core/asgi.py new file mode 100644 index 0000000..03b888c --- /dev/null +++ b/yoo/core/asgi.py @@ -0,0 +1,11 @@ +"""generated with djinit""" + +import os +from dotenv import load_dotenv +from django.core.asgi import get_asgi_application + +load_dotenv() + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", os.getenv("DJANGO_SETTINGS_MODULE")) + +application = get_asgi_application() diff --git a/yoo/core/settings/__init__.py b/yoo/core/settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/yoo/core/settings/base.py b/yoo/core/settings/base.py new file mode 100644 index 0000000..9c92304 --- /dev/null +++ b/yoo/core/settings/base.py @@ -0,0 +1,163 @@ +""" +generated with djinit +Common settings shared between development and production environment +""" + +import os +from datetime import timedelta +from pathlib import Path + +from dotenv import load_dotenv + +load_dotenv() + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent.parent + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/ + + +# Application definition +THIRD_PARTY_APPS = [ + "rest_framework", + "rest_framework_simplejwt", + "rest_framework_simplejwt.token_blacklist", + "corsheaders", + "drf_spectacular", +] + +USER_DEFINED_APPS = [ + "apps.core", + "apps.api", +] + +BUILT_IN_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", +] + +INSTALLED_APPS = BUILT_IN_APPS + THIRD_PARTY_APPS + USER_DEFINED_APPS +INSTALLED_APPS.insert(0, "jazzmin") + +MIDDLEWARE = [ + "corsheaders.middleware.CorsMiddleware", + "django.middleware.security.SecurityMiddleware", + "whitenoise.middleware.WhiteNoiseMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +ROOT_URLCONF = "core.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "core.wsgi.application" + + +# Password validation +# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/5.2/topics/i18n/ + +LANGUAGE_CODE = "en-us" +TIME_ZONE = "UTC" +USE_I18N = True +USE_TZ = True + +# Static files and media files +STATIC_URL = "static/" +STATIC_ROOT = BASE_DIR / "staticfiles" + +MEDIA_URL = "media/" +MEDIA_ROOT = BASE_DIR / "media" + +STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +# REST Framework Settings +REST_FRAMEWORK = { + "DEFAULT_RENDERER_CLASSES": [ + "rest_framework.renderers.JSONRenderer", + ], + "DEFAULT_PARSER_CLASSES": [ + "rest_framework.parsers.JSONParser", + ], + "DEFAULT_AUTHENTICATION_CLASSES": ( + "rest_framework_simplejwt.authentication.JWTAuthentication", + "rest_framework.authentication.SessionAuthentication", + ), + "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", + "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", + "PAGE_SIZE": 20, +} + +# DRF Spectacular settings +SPECTACULAR_SETTINGS = { + "TITLE": "core API", + "DESCRIPTION": "API documentation for core", + "VERSION": "1.0.0", + "SERVE_INCLUDE_SCHEMA": False, + "COMPONENT_SPLIT_REQUEST": True, + "SCHEMA_PATH_PREFIX": "/api/", +} + +# JWT Settings +SIMPLE_JWT = { + "ACCESS_TOKEN_LIFETIME": timedelta(days=1), + "REFRESH_TOKEN_LIFETIME": timedelta(days=3), + "ROTATE_REFRESH_TOKENS": True, + "BLACKLIST_AFTER_ROTATION": True, +} + +# CORS Settings +CORS_ALLOWED_ORIGINS = [ + "http://localhost:3000", + "http://127.0.0.1:3000", +] + +CORS_ALLOW_CREDENTIALS = True + +# Security Settings +SECURE_BROWSER_XSS_FILTER = True +SECURE_CONTENT_TYPE_NOSNIFF = True +X_FRAME_OPTIONS = "DENY" diff --git a/yoo/core/settings/development.py b/yoo/core/settings/development.py new file mode 100644 index 0000000..f78a937 --- /dev/null +++ b/yoo/core/settings/development.py @@ -0,0 +1,29 @@ +"""generated with djinit""" + +from .base import * + +SECRET_KEY = "4RxQW##Ss0h(8&Q3snx(2Km(qw75kRjGM#-zyV7*KUV&3)rUGZ" + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = ["*"] + +# Database +# https://docs.djangoproject.com/en/5.2/ref/settings/#databases + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", + } +} + +# Email backend for development +EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" + +# CORS settings for development +CORS_ALLOW_ALL_ORIGINS = True + +# Disable HTTPS redirect in development +SECURE_SSL_REDIRECT = False diff --git a/yoo/core/settings/production.py b/yoo/core/settings/production.py new file mode 100644 index 0000000..e0cd36c --- /dev/null +++ b/yoo/core/settings/production.py @@ -0,0 +1,47 @@ +"""generated with djinit""" + +from .base import * +import os +import dj_database_url + +DEBUG = False +SECRET_KEY = os.getenv("SECRET_KEY") +ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "").split(",") + +# Database +# Using individual database parameters +DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": os.getenv("DB_NAME"), + "USER": os.getenv("DB_USER"), + "PASSWORD": os.getenv("DB_PASSWORD"), + "HOST": os.getenv("DB_HOST"), + "PORT": os.getenv("DB_PORT"), + } +} + + +# Email settings +EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" +EMAIL_HOST = os.getenv("EMAIL_HOST") +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 +SECURE_HSTS_SECONDS = 31536000 +SECURE_HSTS_INCLUDE_SUBDOMAINS = True +SECURE_HSTS_PRELOAD = True +SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") + +# Session settings +SESSION_COOKIE_SECURE = True +CSRF_COOKIE_SECURE = True + +# Static files +STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" diff --git a/yoo/core/urls.py b/yoo/core/urls.py new file mode 100644 index 0000000..662a94c --- /dev/null +++ b/yoo/core/urls.py @@ -0,0 +1,30 @@ +"""generated with djinit""" + +from django.contrib import admin +from django.urls import path, include +from django.conf import settings +from drf_spectacular.views import ( + SpectacularAPIView, + SpectacularSwaggerView, +) +from rest_framework_simplejwt.views import ( + TokenObtainPairView, + TokenRefreshView, + TokenBlacklistView, +) + +urlpatterns = [ + path("admin/", admin.site.urls), + # JWT tokens + path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"), + path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), + path("token/blacklist/", TokenBlacklistView.as_view(), name="token_blacklist"), + path("api/", include("apps.api.urls")), +] + +# Don't show schema in production +if settings.DEBUG: + urlpatterns += [ + path("schema/", SpectacularAPIView.as_view(), name="schema"), + path("docs/", SpectacularSwaggerView.as_view(url_name="schema")), + ] diff --git a/yoo/core/wsgi.py b/yoo/core/wsgi.py new file mode 100644 index 0000000..fa32c6a --- /dev/null +++ b/yoo/core/wsgi.py @@ -0,0 +1,11 @@ +"""generated with djinit""" + +import os +from dotenv import load_dotenv +from django.core.wsgi import get_wsgi_application + +load_dotenv() + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", os.getenv("DJANGO_SETTINGS_MODULE")) + +application = get_wsgi_application() diff --git a/yoo/justfile b/yoo/justfile new file mode 100644 index 0000000..2299604 --- /dev/null +++ b/yoo/justfile @@ -0,0 +1,154 @@ +# Justfile for yoo +# Task runner for Django development +# Install just: https://github.com/casey/just + +# Default task: show available commands +default: + @just --list + +# Run development server +dev: + uv run python manage.py runserver + +# Run development server with custom host and port +dev-server HOST="0.0.0.0" PORT="8000": + uv run python manage.py runserver {{HOST}}:{{PORT}} + +# Create migrations for all apps +makemigrations: + uv run python manage.py makemigrations + +# Create migrations for specific app +makemigrations-app APP: + uv run python manage.py makemigrations {{APP}} + +# Run all migrations +migrate: + uv run python manage.py migrate + +# Reset database (delete all migrations) +reset-db: + uv run python manage.py flush --noinput + uv run python manage.py migrate + +# Create superuser +createsuperuser: + uv run python manage.py createsuperuser + +# Collect static files +collectstatic: + uv run python manage.py collectstatic --noinput + +# Run Django shell +shell: + uv run python manage.py shell + +# Run Django shell plus (IPython) +shell-plus: + uv run python manage.py shell_plus + +# Run tests +test: + uv run python manage.py test + +# Run tests with coverage +test-coverage: + uv run pytest --cov=. --cov-report=html --cov-report=term + +# Lint code with ruff +lint: + uv run ruff check . + +# Format code with ruff +format: + uv run ruff format . + +# Lint and format code +style: + uv run ruff check . + uv run ruff format . + +# Check security issues +check: + uv run python manage.py check + +# Check security issues (production settings) +check-deploy: + uv run python manage.py check --deploy + +# Show all database migrations status +showmigrations: + uv run python manage.py showmigrations + +# Sync dependencies (uv equivalent of pip freeze) +requirements: + uv pip compile + +# Run all linting and checks +ci: + uv run ruff check . + uv run ruff format --check . + uv run python manage.py check + +# Start interactive shell +shell-ipython: + uv run python manage.py shell_plus + +# Clear all cache +clear-cache: + uv run python manage.py clear_cache + +# Show Django version +version: + uv run python manage.py version + +# Run database shell +dbshell: + uv run python manage.py dbshell + +# Start production server with gunicorn +server: + uv run gunicorn yoo.wsgi:application --bind 0.0.0.0:8000 + +# Start production server with gunicorn workers +server-prod WORKERS="4": + uv run gunicorn yoo.wsgi:application --bind 0.0.0.0:8000 --workers {{WORKERS}} + +# Start production server with gunicorn and reload on file changes +server-watch: + uv run gunicorn yoo.wsgi:application --bind 0.0.0.0:8000 --reload + +# Clean Python cache files +clean: + find . -type d -name "__pycache__" -exec rm -rf {} + + find . -type f -name "*.pyc" -delete + find . -type f -name "*.pyo" -delete + find . -type f -name ".coverage" -delete + find . -type d -name "*.egg-info" -exec rm -rf {} + + rm -rf htmlcov/ + rm -rf .pytest_cache/ + +# Setup project for development (using uv) +setup: + uv sync + uv run python manage.py migrate + uv run python manage.py createsuperuser + +# Complete setup including static files +setup-complete: + uv sync + uv run python manage.py migrate + uv run python manage.py collectstatic --noinput + uv run python manage.py createsuperuser + +# Install a new package +install PACKAGE: + uv add {{PACKAGE}} + +# Install a dev package +install-dev PACKAGE: + uv add --dev {{PACKAGE}} + +# Remove a package +remove PACKAGE: + uv remove {{PACKAGE}} diff --git a/yoo/manage.py b/yoo/manage.py new file mode 100755 index 0000000..422a555 --- /dev/null +++ b/yoo/manage.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python + +"""generated with djinit""" + +"""Django's command-line utility for administrative tasks.""" +import os +import sys + +from dotenv import load_dotenv + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault("DJANGO_SETTINGS_MODULE", os.getenv("DJANGO_SETTINGS_MODULE")) + + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + load_dotenv() + main() diff --git a/yoo/pyproject.toml b/yoo/pyproject.toml new file mode 100644 index 0000000..4e6bf5e --- /dev/null +++ b/yoo/pyproject.toml @@ -0,0 +1,41 @@ +[project] +name = "yoo" +version = "0.1.0" +description = "" +readme = "README.md" +requires-python = ">=3.13" +license = { text = "MIT" } +dependencies = [ + "django", + "python-dotenv", + "django-jazzmin", + "djangorestframework", + "djangorestframework-simplejwt", + "drf-spectacular", + "django-cors-headers", + "whitenoise", + "psycopg2-binary", + "gunicorn", +] + +[tool.ruff] +line-length = 88 +target-version = "py38" + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "UP", # pyupgrade +] +ignore = [ + "E501", # line too long, handled by formatter + "B008", # do not perform function calls in argument defaults +] + +[tool.ruff.lint.isort] +known-first-party = ["yoo"] diff --git a/yoo/requirements.txt b/yoo/requirements.txt new file mode 100644 index 0000000..5d8a32d --- /dev/null +++ b/yoo/requirements.txt @@ -0,0 +1,14 @@ +#--------------------------------------------------# +# The following was generated by djinit: # +#--------------------------------------------------# + +Django +python-dotenv +django-jazzmin +djangorestframework +djangorestframework_simplejwt +drf-spectacular +django-cors-headers +whitenoise +psycopg2-binary +gunicorn diff --git a/yoo/runtime.txt b/yoo/runtime.txt new file mode 100644 index 0000000..e912aa6 --- /dev/null +++ b/yoo/runtime.txt @@ -0,0 +1 @@ +python-3.13