From 172cfd2a2239407007b8818bac182d71b51d2b95 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Tue, 28 Oct 2025 22:55:23 +0545 Subject: [PATCH] feat: add file and project templates for djinit --- src/scripts/app_generator.py | 2 +- src/templates/files.py | 412 +++++++++++++++++++++++++++++++++++ src/templates/project.py | 105 +++++++++ src/templates/settings.py | 309 ++++++++++++++++++++++++++ 4 files changed, 827 insertions(+), 1 deletion(-) create mode 100644 src/templates/files.py create mode 100644 src/templates/project.py create mode 100644 src/templates/settings.py diff --git a/src/scripts/app_generator.py b/src/scripts/app_generator.py index 45286f4..214e618 100644 --- a/src/scripts/app_generator.py +++ b/src/scripts/app_generator.py @@ -1,5 +1,5 @@ """ -App management for Django project setup. +App management for djinit. Handles creation of Django apps and updating settings. """ diff --git a/src/templates/files.py b/src/templates/files.py new file mode 100644 index 0000000..b389ee7 --- /dev/null +++ b/src/templates/files.py @@ -0,0 +1,412 @@ +""" +File management utilities for djinit. +Handles creation and modification of project files. +""" + +import os + +from src.utils import change_cwd, create_file_with_content + + +class FileManager: + def __init__(self, project_root: str, project_name: str, app_names: list): + self.project_root = project_root + self.project_name = project_name + self.app_names = app_names + self.project_configs = os.path.join(project_root, project_name) + self.settings_folder = os.path.join(self.project_configs, "settings") + self.settings_file = os.path.join(self.project_configs, "settings.py") + + def create_gitignore(self) -> bool: + with change_cwd(self.project_root): + gitignore_content = [ + "# 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", + ] + + return create_file_with_content( + ".gitignore", + "\n".join(gitignore_content) + "\n", + "Created .gitignore file", + ) + + def create_requirements(self) -> bool: + with change_cwd(self.project_root): + requirements = [ + "Django>=5.2.7", + "python-dotenv>=1.1.1", + "django-jazzmin>=3.0.1", + "djangorestframework>=3.16.1", + "djangorestframework_simplejwt>=5.5.1", + "drf-spectacular>=0.28.0", + "django-cors-headers>=4.9.0", + "whitenoise>=6.8.2", + "psycopg2-binary>=2.9.9", # For PostgreSQL support + "gunicorn>=21.2.0", # For production deployment + ] + + return create_file_with_content( + "requirements.txt", + "\n".join(requirements) + "\n", + "Created requirements.txt with Django dependencies", + ) + + def create_readme(self) -> bool: + with change_cwd(self.project_root): + readme_content = f"""# {self.project_name} + +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 + +``` +{self.project_name}/ +├── {self.project_name}/ +│ ├── settings/ +│ │ ├── base.py +│ │ ├── development.py +│ │ └── production.py +│ ├── urls.py +│ ├── wsgi.py +│ └── asgi.py +{"".join([f"├── {app}/\n" for app in self.app_names])}├── manage.py +├── requirements.txt +├── pyproject.toml +├── .env.sample +└── 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/ +""" + + return create_file_with_content( + "README.md", + readme_content, + "Created README.md file", + ) + + def create_env_file(self) -> bool: + """Create .env.sample file with environment variables.""" + with change_cwd(self.project_root): + env_content = f"""# Django settings +DJANGO_SETTINGS_MODULE={self.project_name}.settings.development +SECRET_KEY= your-secret-key +ALLOWED_HOSTS=api.your-domain.com,www.your-domain.com + +# Database +DB_NAME= +DB_USER= +DB_PASSWORD= +DB_HOST= +DB_PORT= +""" + + return create_file_with_content( + ".env.sample", + env_content, + "Created .env.sample file with generated secret key", + ) + + def create_pyproject_toml(self, metadata: dict) -> bool: + with change_cwd(self.project_root): + pyproject_content = f"""[project] +name = "{metadata["package_name"]}" +version = "0.1.0" +description = "" +readme = "README.md" +requires-python = ">=3.13" +license = {{ text = "MIT }} +dependencies = [ + "django>=5.2.7", + "python-dotenv>=1.1.1", + "django-jazzmin>=3.0.1", + "djangorestframework>=3.16.1", + "djangorestframework-simplejwt>=5.5.1", + "drf-spectacular>=0.28.0", + "django-cors-headers>=4.9.0", + "whitenoise>=6.8.2", + "psycopg2-binary>=2.9.9", + "gunicorn>=21.2.0", +] + +[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 = ["{self.project_name}"] +""" + + return create_file_with_content( + "pyproject.toml", + pyproject_content, + "Created pyproject.toml file for uv", + ) + + def create_app_urls(self) -> bool: + for app_name in self.app_names: + app_path = os.path.join(self.project_root, app_name) + with change_cwd(app_path): + urls_content = f"""from django.urls import path +from . import views + +app_name = '{app_name}' + +urlpatterns = [ + # Add your URL patterns here + # path('', views.index, name='index'), +] +""" + + create_file_with_content( + "urls.py", + urls_content, + f"Created {app_name}/urls.py", + should_format=True, + ) + return True + + def update_project_urls(self) -> bool: + with change_cwd(self.project_configs): + # Generate URL includes for all apps + app_urls = "\n ".join([f'path("", include("{app_name}.urls")),' for app_name in self.app_names]) + + urls_content = f"""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"), + # App URLs + {app_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")), + ] +""" + + result = create_file_with_content( + "urls.py", + urls_content, + "Updated project urls.py with comprehensive URL configuration", + should_format=True, + ) + return result + + def update_wsgi_file(self) -> bool: + with change_cwd(self.project_configs): + wsgi_content = """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() +""" + + return create_file_with_content( + "wsgi.py", + wsgi_content, + "Updated wsgi.py with custom configuration", + should_format=True, + ) + + def update_asgi_file(self) -> bool: + with change_cwd(self.project_configs): + asgi_content = """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() +""" + + return create_file_with_content( + "asgi.py", + asgi_content, + "Updated asgi.py with custom configuration", + should_format=True, + ) + + def update_manage_py(self) -> bool: + """Update manage.py with custom configuration.""" + with change_cwd(self.project_root): + manage_content = """#!/usr/bin/env python +\"\"\"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() +""" + + return create_file_with_content( + "manage.py", + manage_content, + "Updated manage.py with custom configuration", + should_format=True, + ) diff --git a/src/templates/project.py b/src/templates/project.py new file mode 100644 index 0000000..8be20fa --- /dev/null +++ b/src/templates/project.py @@ -0,0 +1,105 @@ +""" +Project management for djinit. +Handles creation of Django projects and apps. +""" + +import os +import subprocess +import sys + +from src.scripts.console_ui import UIFormatter +from src.utils import change_cwd + + +class ProjectManager: + def __init__(self, project_dir: str, project_name: str, app_names: list): + self.project_dir = project_dir + self.project_name = project_name + self.app_names = app_names + self.project_root = os.path.join(os.getcwd(), project_dir) + + def create_project(self) -> bool: + # Check if project already exists + if os.path.exists(self.project_root): + UIFormatter.print_error(f"Directory '{self.project_dir}' already exists.") + UIFormatter.print_info("Please choose a different project directory name or remove the existing directory.") + return False + + # Ensure Django is installed + self._ensure_django_installed() + + # Create the project directory first + os.makedirs(self.project_root, exist_ok=True) + + # Create Django project inside the directory + subprocess.run( + ["django-admin", "startproject", self.project_name, self.project_root], + check=True, + ) + + UIFormatter.print_success(f"Django project '{self.project_name}' created successfully!") + return True + + def create_apps(self) -> bool: + with change_cwd(self.project_root): + # Create each app + for app_name in self.app_names: + try: + subprocess.run( + [ + sys.executable, + os.path.join(self.project_root, "manage.py"), + "startapp", + app_name, + ], + check=True, + ) + UIFormatter.print_success(f"Django app '{app_name}' created successfully!") + except subprocess.CalledProcessError as e: + UIFormatter.print_error(f"Failed to create app '{app_name}': {e}") + return False + + return True + + def _ensure_django_installed(self) -> None: + try: + import django + except ImportError: + UIFormatter.print_info("Django not found, installing...") + subprocess.run( + [sys.executable, "-m", "pip", "install", "--upgrade", "django"], + check=True, + ) + UIFormatter.print_success("Django installed successfully!") + + 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.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"), + ] + + # Add required files for each app + for app_name in self.app_names: + app_files = [ + os.path.join(self.project_root, app_name, "__init__.py"), + os.path.join(self.project_root, app_name, "apps.py"), + os.path.join(self.project_root, app_name, "models.py"), + os.path.join(self.project_root, app_name, "views.py"), + os.path.join(self.project_root, app_name, "admin.py"), + ] + required_files.extend(app_files) + + missing_files = [file_path for file_path in required_files if not os.path.exists(file_path)] + + if missing_files: + UIFormatter.print_error("Project structure validation failed:") + for file_path in missing_files: + UIFormatter.print_error(f" Missing: {file_path}") + return False + + UIFormatter.print_success("Project structure validation passed") + return True diff --git a/src/templates/settings.py b/src/templates/settings.py new file mode 100644 index 0000000..b56d3a4 --- /dev/null +++ b/src/templates/settings.py @@ -0,0 +1,309 @@ +""" +Settings management for djinit. +Handles creation and configuration of Django settings files. +""" + +import os + +from src.scripts.console_ui import UIFormatter +from src.scripts.secretkey_generator import generate_secret_key +from src.utils import change_cwd, create_file_with_content + + +class SettingsManager: + def __init__(self, project_root: str, project_name: str, app_names: list): + self.project_root = project_root + self.project_name = project_name + self.app_names = app_names + self.project_configs = os.path.join(project_root, project_name) + self.settings_folder = os.path.join(self.project_configs, "settings") + self.settings_file = os.path.join(self.project_configs, "settings.py") + self.secret_key = generate_secret_key(50) + + def create_settings_structure(self) -> bool: + # Change to project configs directory + with change_cwd(self.project_configs): + # Create settings folder + os.makedirs("settings", exist_ok=True) + + # Move settings.py to settings/base.py + if os.path.exists(self.settings_file): + os.rename(self.settings_file, os.path.join(self.settings_folder, "base.py")) + + # Create __init__.py files + with change_cwd(self.settings_folder): + open("__init__.py", "a").close() + open("development.py", "a").close() + open("production.py", "a").close() + + UIFormatter.print_success("Created Django settings folder structure") + return True + + def update_base_settings(self) -> bool: + with change_cwd(self.settings_folder): + # Generate app list from app_names + app_list_lines = [f' "{app_name}",' for app_name in self.app_names] + app_list = "\n".join(app_list_lines) + + base_content = f'''""" +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 = [ +{app_list} +] + +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 = "{self.project_name}.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 = "{self.project_name}.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": "{self.project_name} API", + "DESCRIPTION": "API documentation for {self.project_name}", + "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" + +''' + + return create_file_with_content( + "base.py", + base_content, + "Updated settings/base.py with comprehensive configuration", + should_format=True, + ) + + def update_development_settings(self) -> bool: + """Update development.py with development-specific settings.""" + with change_cwd(self.settings_folder): + dev_content = f"""from .base import * + +SECRET_KEY = "{self.secret_key}" + +# 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 +""" + + return create_file_with_content( + "development.py", + dev_content, + "Updated settings/development.py", + should_format=True, + ) + + def update_production_settings(self) -> bool: + """Update production.py with production-specific settings.""" + with change_cwd(self.settings_folder): + prod_content = """from .base import * +import os + +DEBUG = False +SECRET_KEY = os.getenv('SECRET_KEY') +ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', '').split(',') + +# Database +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') + +# 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' + +""" + + return create_file_with_content( + "production.py", + prod_content, + "Updated settings/production.py", + should_format=True, + )