diff --git a/djinit/cli.py b/djinit/cli.py index f17d3a3..a706956 100644 --- a/djinit/cli.py +++ b/djinit/cli.py @@ -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 diff --git a/djinit/generator.py b/djinit/generator.py index 2a6691d..f1e9754 100644 --- a/djinit/generator.py +++ b/djinit/generator.py @@ -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) diff --git a/djinit/scripts/app_generator.py b/djinit/scripts/app_generator.py index 5ed1037..0b319df 100644 --- a/djinit/scripts/app_generator.py +++ b/djinit/scripts/app_generator.py @@ -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 diff --git a/djinit/scripts/console_ui.py b/djinit/scripts/console_ui.py index 7346822..b0d796e 100644 --- a/djinit/scripts/console_ui.py +++ b/djinit/scripts/console_ui.py @@ -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 diff --git a/djinit/scripts/files.py b/djinit/scripts/files.py index 8c0dda4..a47355b 100644 --- a/djinit/scripts/files.py +++ b/djinit/scripts/files.py @@ -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", diff --git a/djinit/scripts/input_handler.py b/djinit/scripts/input_handler.py index a4915f6..c3afd5b 100644 --- a/djinit/scripts/input_handler.py +++ b/djinit/scripts/input_handler.py @@ -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}]") diff --git a/djinit/scripts/project.py b/djinit/scripts/project.py index d963c12..29ba578 100644 --- a/djinit/scripts/project.py +++ b/djinit/scripts/project.py @@ -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: diff --git a/djinit/templates/custom/api/urls.j2 b/djinit/templates/custom/api/urls.j2 new file mode 100644 index 0000000..69585ec --- /dev/null +++ b/djinit/templates/custom/api/urls.j2 @@ -0,0 +1,9 @@ +from django.urls import include, path + +app_name = "api" + +urlpatterns = [ + path("v1/", include("api.v1.urls")), +] + + diff --git a/djinit/templates/custom/api/v1_urls.j2 b/djinit/templates/custom/api/v1_urls.j2 new file mode 100644 index 0000000..40ebdea --- /dev/null +++ b/djinit/templates/custom/api/v1_urls.j2 @@ -0,0 +1,9 @@ +from django.urls import include, path + +app_name = "v1" + +urlpatterns = [ + path("users/", include("apps.users.urls")), +] + + diff --git a/djinit/templates/custom/app/models_generic.j2 b/djinit/templates/custom/app/models_generic.j2 new file mode 100644 index 0000000..8067069 --- /dev/null +++ b/djinit/templates/custom/app/models_generic.j2 @@ -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 + + diff --git a/djinit/templates/custom/app/serializers_generic.j2 b/djinit/templates/custom/app/serializers_generic.j2 new file mode 100644 index 0000000..ef60bcb --- /dev/null +++ b/djinit/templates/custom/app/serializers_generic.j2 @@ -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__" + + diff --git a/djinit/templates/custom/app/services_generic.j2 b/djinit/templates/custom/app/services_generic.j2 new file mode 100644 index 0000000..eb75f90 --- /dev/null +++ b/djinit/templates/custom/app/services_generic.j2 @@ -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 + + diff --git a/djinit/templates/custom/app/tests_generic.j2 b/djinit/templates/custom/app/tests_generic.j2 new file mode 100644 index 0000000..910bad8 --- /dev/null +++ b/djinit/templates/custom/app/tests_generic.j2 @@ -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() == [] + + diff --git a/djinit/templates/custom/app/urls_generic.j2 b/djinit/templates/custom/app/urls_generic.j2 new file mode 100644 index 0000000..68578e9 --- /dev/null +++ b/djinit/templates/custom/app/urls_generic.j2 @@ -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)), +] + + diff --git a/djinit/templates/custom/app/views_generic.j2 b/djinit/templates/custom/app/views_generic.j2 new file mode 100644 index 0000000..af3545c --- /dev/null +++ b/djinit/templates/custom/app/views_generic.j2 @@ -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 + + diff --git a/djinit/templates/custom/core/exceptions.j2 b/djinit/templates/custom/core/exceptions.j2 new file mode 100644 index 0000000..3a2f7fa --- /dev/null +++ b/djinit/templates/custom/core/exceptions.j2 @@ -0,0 +1,7 @@ +class ApplicationError(Exception): + """Base application exception.""" + + def __init__(self, message: str): + super().__init__(message) + + diff --git a/djinit/templates/custom/core/middleware_request_logger.j2 b/djinit/templates/custom/core/middleware_request_logger.j2 new file mode 100644 index 0000000..00ed0fe --- /dev/null +++ b/djinit/templates/custom/core/middleware_request_logger.j2 @@ -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 + + diff --git a/djinit/templates/custom/core/mixins_timestamped_model.j2 b/djinit/templates/custom/core/mixins_timestamped_model.j2 new file mode 100644 index 0000000..3d27df1 --- /dev/null +++ b/djinit/templates/custom/core/mixins_timestamped_model.j2 @@ -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 + + diff --git a/djinit/templates/custom/core/utils_responses.j2 b/djinit/templates/custom/core/utils_responses.j2 new file mode 100644 index 0000000..fc6bf7a --- /dev/null +++ b/djinit/templates/custom/core/utils_responses.j2 @@ -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) + + diff --git a/djinit/templates/custom/project_urls_api.j2 b/djinit/templates/custom/project_urls_api.j2 new file mode 100644 index 0000000..459de61 --- /dev/null +++ b/djinit/templates/custom/project_urls_api.j2 @@ -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")), +] + + diff --git a/djinit/templates/custom/users/models_user.j2 b/djinit/templates/custom/users/models_user.j2 new file mode 100644 index 0000000..556619b --- /dev/null +++ b/djinit/templates/custom/users/models_user.j2 @@ -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 + + diff --git a/djinit/templates/custom/users/serializers_user.j2 b/djinit/templates/custom/users/serializers_user.j2 new file mode 100644 index 0000000..ca7a68e --- /dev/null +++ b/djinit/templates/custom/users/serializers_user.j2 @@ -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"] + + diff --git a/djinit/templates/custom/users/services_user.j2 b/djinit/templates/custom/users/services_user.j2 new file mode 100644 index 0000000..5888378 --- /dev/null +++ b/djinit/templates/custom/users/services_user.j2 @@ -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 + + diff --git a/djinit/templates/custom/users/tests_user_api.j2 b/djinit/templates/custom/users/tests_user_api.j2 new file mode 100644 index 0000000..3bdb897 --- /dev/null +++ b/djinit/templates/custom/users/tests_user_api.j2 @@ -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() == [] + + diff --git a/djinit/templates/custom/users/urls.j2 b/djinit/templates/custom/users/urls.j2 new file mode 100644 index 0000000..98e7d7a --- /dev/null +++ b/djinit/templates/custom/users/urls.j2 @@ -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)), +] + + diff --git a/djinit/templates/custom/users/views_user.j2 b/djinit/templates/custom/users/views_user.j2 new file mode 100644 index 0000000..660fcc7 --- /dev/null +++ b/djinit/templates/custom/users/views_user.j2 @@ -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) + + diff --git a/djinit/templates/project/settings_production.j2 b/djinit/templates/project/settings_production.j2 index d85ba12..4f7d346 100644 --- a/djinit/templates/project/settings_production.j2 +++ b/djinit/templates/project/settings_production.j2 @@ -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 diff --git a/djinit/templates/shared/env_sample.j2 b/djinit/templates/shared/env_sample.j2 index 135d06b..0a4e621 100644 --- a/djinit/templates/shared/env_sample.j2 +++ b/djinit/templates/shared/env_sample.j2 @@ -21,3 +21,5 @@ EMAIL_PORT= EMAIL_USE_TLS= EMAIL_HOST_USER= EMAIL_HOST_PASSWORD= +DEFAULT_FROM_EMAIL= +SERVER_EMAIL= diff --git a/djinit/templates/shared/github_actions_ci.j2 b/djinit/templates/shared/github_actions_ci.j2 index 52082de..6200b9c 100644 --- a/djinit/templates/shared/github_actions_ci.j2 +++ b/djinit/templates/shared/github_actions_ci.j2 @@ -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 diff --git a/djinit/templates/shared/gitlab_ci.j2 b/djinit/templates/shared/gitlab_ci.j2 index 4fcedd6..ff6ef5b 100644 --- a/djinit/templates/shared/gitlab_ci.j2 +++ b/djinit/templates/shared/gitlab_ci.j2 @@ -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: diff --git a/djinit/templates/shared/readme.j2 b/djinit/templates/shared/readme.j2 index 956aa49..50a5e31 100644 --- a/djinit/templates/shared/readme.j2 +++ b/djinit/templates/shared/readme.j2 @@ -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