diff --git a/pyproject.toml b/pyproject.toml index eb3d56c..89b59f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,15 +27,16 @@ classifiers=[ "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Code Generators", - ], + ] dependencies = [ "click>=8.3.0", "rich>=14.2.0", "ruff>=0.14.2", + "jinja2>=3.1.0", ] -[project.url] +[project.urls] Homepage = "https://github.com/S4NKALP/djinit" -# [porject.scripts] -# djinit = "src.generator:main" +[project.scripts] +djinit = "src.generator:main" diff --git a/requirements.txt b/requirements.txt index 4e4d668..18b6b3f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ click==8.3.0 rich==14.2.0 ruff==0.14.2 +jinja2>=3.1.0 diff --git a/setup.py b/setup.py index d165485..373c5c2 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,9 @@ setup( "ruff>=0.14.2", "click>=8.3.0", "rich>=14.2.0", - "django-environ==0.11.2", + "jinja2>=3.1.0", + # "django<=5.2.7", + ], entry_points={"console_scripts": ["djinit=src.generator:main"]}, classifiers=[ diff --git a/src/cli.py b/src/cli.py index 4df2415..1676dca 100644 --- a/src/cli.py +++ b/src/cli.py @@ -9,10 +9,10 @@ from typing import Callable, List, Tuple from src.scripts.cicd_config_generator import CICDConfigGenerator from src.scripts.console_ui import UIFormatter from src.scripts.deployment_files_generator import DeploymentConfigGenerator +from src.scripts.files import FileManager +from src.scripts.project import ProjectManager from src.scripts.secretkey_generator import generate_secret_command -from src.templates.files import FileManager -from src.templates.project import ProjectManager -from src.templates.settings import SettingsManager +from src.scripts.settings import SettingsManager class Cli: @@ -25,9 +25,9 @@ class Cli: self.project_root = os.path.join(os.getcwd(), project_dir) # Initialize managers - self.project_manager = ProjectManager(project_dir, project_name, app_names) - self.settings_manager = SettingsManager(self.project_root, project_name, app_names) - self.file_manager = FileManager(self.project_root, project_name, app_names) + self.project_manager = ProjectManager(project_dir, project_name, app_names, metadata) + self.settings_manager = SettingsManager(self.project_root, project_name, app_names, metadata) + self.file_manager = FileManager(self.project_root, project_name, app_names, metadata) self.deployment_manager = DeploymentConfigGenerator(self.project_root, project_name) self.cicd_manager = CICDConfigGenerator(self.project_root, project_name) diff --git a/src/scripts/cicd_config_generator.py b/src/scripts/cicd_config_generator.py index 957d407..6a8ff68 100644 --- a/src/scripts/cicd_config_generator.py +++ b/src/scripts/cicd_config_generator.py @@ -4,10 +4,12 @@ Handles creation of Github Actions and GitLab CI onfigurations """ import os + +from src.scripts.template_engine import template_engine from src.utils import change_cwd, create_file_with_content -class CiCDConfigGenerator: +class CICDConfigGenerator: def __init__(self, project_root: str, project_name: str): self.project_root = project_root self.project_name = project_name @@ -17,117 +19,8 @@ class CiCDConfigGenerator: github_dir = os.path.join(self.project_root, ".github", "workflows") os.makedirs(github_dir, exist_ok=True) - workflow_content = f"""name: Django CI - -on: - push: - branches: [ main, develop ] - pull_request: - branches: [ main, develop ] - -jobs: - test: - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.10", "3.11", "3.12", "3.13"] - - services: - postgres: - image: postgres:15 - env: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: test_db - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - ports: - - 5432:5432 - - steps: - - uses: actions/checkout@v4 - - - name: Set up Python ${{{{ matrix.python-version }}}} - uses: actions/setup-python@v5 - with: - python-version: ${{{{ matrix.python-version }}}} - - - name: Install uv - uses: astral-sh/setup-uv@v4 - with: - version: "latest" - - - name: Install dependencies - run: uv sync - - - name: Run linting - run: uv run ruff check . - - - name: Format check - run: uv run ruff format --check . - - - name: Set up PostgreSQL - env: - DB_NAME: test_db - DB_USER: postgres - DB_PASSWORD: postgres - DB_HOST: localhost - DB_PORT: 5432 - run: | - echo "PostgreSQL is ready" - - - name: Run Django tests - env: - DJANGO_SETTINGS_MODULE: {self.project_name}.settings.development - SECRET_KEY: test-secret-key-for-ci - ALLOWED_HOSTS: localhost - DB_NAME: test_db - DB_USER: postgres - DB_PASSWORD: postgres - DB_HOST: localhost - DB_PORT: 5432 - run: | - uv run python manage.py check - uv run python manage.py test - - - name: Check migrations - env: - DJANGO_SETTINGS_MODULE: {self.project_name}.settings.development - SECRET_KEY: test-secret-key-for-ci - ALLOWED_HOSTS: localhost - DB_NAME: test_db - DB_USER: postgres - DB_PASSWORD: postgres - DB_HOST: localhost - DB_PORT: 5432 - run: | - uv run python manage.py makemigrations --check --dry-run - - security: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.13" - - - name: Install uv - uses: astral-sh/setup-uv@v4 - with: - version: "latest" - - - name: Install dependencies - run: uv sync - - - name: Run security check - run: uv run python manage.py check --deploy -""" - + context = {"project_name": self.project_name} + workflow_content = template_engine.render_template("github_actions_ci.j2", context) workflow_file = os.path.join(github_dir, "ci.yml") create_file_with_content( workflow_file, @@ -138,141 +31,9 @@ jobs: def create_gitlab_ci(self) -> bool: with change_cwd(self.project_root): - gitlab_ci_content = f"""# GitLab CI/CD configuration for {self.project_name} - -stages: - - test - - lint - - security - -variables: - POSTGRES_DB: test_db - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/test_db" - -# Use Docker-in-Docker -services: - - postgres:15 - -before_script: - - python --version - - pip install uv - - uv sync - -# Lint stage -lint: - stage: lint - image: python:3.13 - script: - - uv run ruff check . - - uv run ruff format --check . - only: - - branches - - merge_requests - -# Test stage -test: - stage: test - image: python:3.13 - variables: - DJANGO_SETTINGS_MODULE: {self.project_name}.settings.development - SECRET_KEY: test-secret-key-for-ci - ALLOWED_HOSTS: localhost - script: - - uv run python manage.py check - - uv run python manage.py migrate - - uv run python manage.py test - only: - - branches - - merge_requests - -# Multiple Python versions -test:python310: - stage: test - image: python:3.10 - variables: - DJANGO_SETTINGS_MODULE: {self.project_name}.settings.development - SECRET_KEY: test-secret-key-for-ci - ALLOWED_HOSTS: localhost - script: - - pip install uv - - uv sync - - uv run python manage.py check - - uv run python manage.py migrate - - uv run python manage.py test - only: - - branches - - merge_requests - -test:python311: - stage: test - image: python:3.11 - variables: - DJANGO_SETTINGS_MODULE: {self.project_name}.settings.development - SECRET_KEY: test-secret-key-for-ci - ALLOWED_HOSTS: localhost - script: - - pip install uv - - uv sync - - uv run python manage.py check - - uv run python manage.py migrate - - uv run python manage.py test - only: - - branches - - merge_requests - -test:python312: - stage: test - image: python:3.12 - variables: - DJANGO_SETTINGS_MODULE: {self.project_name}.settings.development - SECRET_KEY: test-secret-key-for-ci - ALLOWED_HOSTS: localhost - script: - - pip install uv - - uv sync - - uv run python manage.py check - - uv run python manage.py migrate - - uv run python manage.py test - only: - - branches - - merge_requests - -test:python313: - stage: test - image: python:3.13 - variables: - DJANGO_SETTINGS_MODULE: {self.project_name}.settings.development - SECRET_KEY: test-secret-key-for-ci - ALLOWED_HOSTS: localhost - script: - - pip install uv - - uv sync - - uv run python manage.py check - - uv run python manage.py migrate - - uv run python manage.py test - only: - - branches - - merge_requests - -# Security check -security: - stage: security - image: python:3.13 - variables: - DJANGO_SETTINGS_MODULE: {self.project_name}.settings.development - SECRET_KEY: test-secret-key-for-ci - script: - - pip install uv - - uv sync - - uv run python manage.py check --deploy - - uv run python manage.py validate_templates - only: - - main - - develop -""" - config_file = ".gitlab_ci_content" + context = {"project_name": self.project_name} + gitlab_ci_content = template_engine.render_template("gitlab_ci.j2", context) + config_file = ".gitlab-ci.yml" create_file_with_content( config_file, gitlab_ci_content, diff --git a/src/scripts/deployment_files_generator.py b/src/scripts/deployment_files_generator.py index de17874..5f0c1bd 100644 --- a/src/scripts/deployment_files_generator.py +++ b/src/scripts/deployment_files_generator.py @@ -3,6 +3,7 @@ Deployment file generator for djinit. Handles creation of Justfile, Procfile, and deployment-related files. """ +from src.scripts.template_engine import template_engine from src.utils import change_cwd, create_file_with_content @@ -13,162 +14,8 @@ class DeploymentConfigGenerator: def create_justfile(self) -> bool: with change_cwd(self.project_root): - justfile_content = f"""# Justfile for {self.project_name} -# 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 {self.project_name}.wsgi:application --bind 0.0.0.0:8000 - -# Start production server with gunicorn workers -server-prod WORKERS="4": - uv run gunicorn {self.project_name}.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 {self.project_name}.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}}}} -""" - + context = {"project_name": self.project_name} + justfile_content = template_engine.render_template("justfile.j2", context) result = create_file_with_content( "justfile", justfile_content, @@ -178,28 +25,8 @@ remove PACKAGE: def create_procfile(self) -> bool: with change_cwd(self.project_root): - procfile_content = f"""# Procfile for {self.project_name} -# 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 {self.project_name}.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 {self.project_name}.wsgi:application --bind 0.0.0.0:$PORT --workers 4 --threads 2 --timeout 120 -# web: gunicorn {self.project_name}.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 {self.project_name} worker --loglevel=info -# beat: celery -A {self.project_name} beat --loglevel=info - -# Development server (for local testing with Foreman/Overmind) -# web: python manage.py runserver 0.0.0.0:$PORT -""" - + context = {"project_name": self.project_name} + procfile_content = template_engine.render_template("procfile.j2", context) result = create_file_with_content( "Procfile", procfile_content, @@ -257,8 +84,8 @@ tmp_upload_dir = None def create_runtime_txt(self, python_version: str = "3.13") -> bool: with change_cwd(self.project_root): - runtime_content = f"""python-{python_version} -""" + context = {"python_version": python_version} + runtime_content = template_engine.render_template("runtime_txt.j2", context) runtime_file = "runtime.txt" result = create_file_with_content( runtime_file, diff --git a/src/scripts/files.py b/src/scripts/files.py new file mode 100644 index 0000000..995a834 --- /dev/null +++ b/src/scripts/files.py @@ -0,0 +1,138 @@ +""" +File management utilities for djinit. +Handles creation and modification of project files. +""" + +import os + +from src.scripts.template_engine import template_engine +from src.utils import change_cwd, create_file_with_content + + +class FileManager: + def __init__(self, project_root: str, project_name: str, app_names: list, metadata: dict): + self.project_root = project_root + self.project_name = project_name + self.app_names = app_names + self.metadata = metadata + 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 = template_engine.render_template("gitignore.j2", {}) + return create_file_with_content( + ".gitignore", + gitignore_content, + "Created .gitignore file", + ) + + def create_requirements(self) -> bool: + with change_cwd(self.project_root): + requirements_content = template_engine.render_template("requirements.j2", {}) + return create_file_with_content( + "requirements.txt", + requirements_content, + "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} + readme_content = template_engine.render_template("readme.j2", context) + 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): + context = {"project_name": self.project_name} + env_content = template_engine.render_template("env_sample.j2", context) + 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): + context = {"package_name": metadata["package_name"], "project_name": self.project_name} + pyproject_content = template_engine.render_template("pyproject_toml.j2", context) + return create_file_with_content( + "pyproject.toml", + pyproject_content, + "Created pyproject.toml file for uv", + ) + + def create_app_urls(self) -> bool: + 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_path = os.path.join(apps_base_dir, app_name) + with change_cwd(app_path): + context = {"app_name": app_name} + urls_content = template_engine.render_template("app_urls.j2", context) + 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): + nested = bool(self.metadata.get("nested_apps")) + nested_dir = self.metadata.get("nested_dir") + effective_app_modules = [ + f"{nested_dir}.{name}" if nested and nested_dir else name for name in self.app_names + ] + context = { + "project_name": self.project_name, + "app_names": effective_app_modules, + } + urls_content = template_engine.render_template("project_urls.j2", context) + 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 = template_engine.render_template("wsgi.j2", {}) + 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 = template_engine.render_template("asgi.j2", {}) + 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 = template_engine.render_template("manage_py.j2", {}) + return create_file_with_content( + "manage.py", + manage_content, + "Updated manage.py with custom configuration", + should_format=True, + ) diff --git a/src/scripts/input_handler.py b/src/scripts/input_handler.py index ad7da31..e894997 100644 --- a/src/scripts/input_handler.py +++ b/src/scripts/input_handler.py @@ -31,6 +31,8 @@ class ProjectMetadata: package_name: str use_github_actions: bool = False use_gitlab_ci: bool = False + nested_apps: bool = False + nested_dir: str | None = None def to_dict(self) -> dict: """Convert to dictionary format.""" @@ -38,6 +40,8 @@ class ProjectMetadata: "package_name": self.package_name, "use_github_actions": self.use_github_actions, "use_gitlab_ci": self.use_gitlab_ci, + "nested_apps": self.nested_apps, + "nested_dir": self.nested_dir, } @@ -109,23 +113,24 @@ class InputCollector: try: console.print(f"[{UIColors.HIGHLIGHT}]App Names[/{UIColors.HIGHLIGHT}]") console.print( - f"[{UIColors.MUTED}]Enter app names separated by commas or press Enter to add one at a time[/{UIColors.MUTED}]" + f"[{UIColors.MUTED}]Enter app names separated by commas (no interactive prompts)[/{UIColors.MUTED}]" ) console.print(f"[{UIColors.MUTED}]Example: users, products, orders[/{UIColors.MUTED}]") user_input = console.input( - f"[{UIColors.HIGHLIGHT}]Enter app names (comma-separated or one at a time):[/{UIColors.HIGHLIGHT}] " + f"[{UIColors.HIGHLIGHT}]Enter app names (comma-separated or single):[/{UIColors.HIGHLIGHT}] " ) - # Empty input - prompt for single app + # Empty input - re-prompt (no interactive flow) if not user_input.strip(): - return self._get_apps_interactive() + UIFormatter.print_error("At least one app name is required") + return self.get_app_names() # Comma-separated input if "," in user_input: return self._parse_comma_separated_apps(user_input) - # Single app with option to add more + # Single app (no additional prompts) return self._get_apps_starting_with(user_input.strip()) except KeyboardInterrupt: @@ -162,33 +167,16 @@ class InputCollector: UIFormatter.print_error(error_msg) return self.get_app_names() - app_names = [first_app] - app_names.extend(self._prompt_for_additional_apps()) - return app_names + return [first_app] def _get_apps_interactive(self) -> list[str]: - app_name = self.get_validated_input("Enter at least one app name", validate_app_name, "app name") - app_names = [app_name] - app_names.extend(self._prompt_for_additional_apps()) - return app_names + """Deprecated: interactive flow disabled. Kept for compatibility.""" + UIFormatter.print_warning("Interactive app addition is disabled. Please enter names comma-separated.") + return self.get_app_names() def _prompt_for_additional_apps(self) -> list[str]: - additional_apps = [] - - while True: - console.print(f"[{UIColors.HIGHLIGHT}]Add another app? (y/N):[/{UIColors.HIGHLIGHT}] ", end="") - sys.stdout.flush() - - response = self.char_reader.get_char() - console.print(response.upper()) - - if response != "y": - break - - next_app = self.get_validated_input("Enter next app name", validate_app_name, "app name") - additional_apps.append(next_app) - - return additional_apps + """Deprecated: interactive flow disabled. Always returns empty list.""" + return [] def get_cicd_choice(self) -> Tuple[bool, bool]: UIFormatter.print_separator() @@ -217,6 +205,30 @@ class InputCollector: else: # Default to none for any unrecognized input return False, False + def get_nested_apps_config(self) -> Tuple[bool, str | None]: + """Ask whether to create nested Django apps and get directory name if yes.""" + UIFormatter.print_separator() + console.print(f"\n[{UIColors.INFO}]Apps Layout[/{UIColors.INFO}]\n") + console.print(f"[{UIColors.MUTED}]Do you want to place apps inside a package directory (e.g., 'apps/')?[/{UIColors.MUTED}]") + console.print(f"[{UIColors.MUTED}]If yes, we'll create that directory as a Python package and generate apps inside it.[/{UIColors.MUTED}]") + console.print() + + console.print(f"[{UIColors.HIGHLIGHT}]Nested Django apps? (y/N):[/{UIColors.HIGHLIGHT}] ", end="") + sys.stdout.flush() + choice = self.char_reader.get_char() + console.print(choice.upper()) + + if choice != "y": + return False, None + + # Ask for directory name + dir_name = self.get_validated_input( + "Enter directory name for apps package (e.g., apps)", + validate_project_name, + "apps package name", + ) + return True, dir_name + class CharReader: """Handles single character input across platforms.""" @@ -271,6 +283,14 @@ def get_user_input() -> Tuple[str, str, str, list, dict]: project_dir = collector.get_validated_input( "Enter project directory name", validate_project_name, "project directory name" ) + + # Check if directory exists and ask for alternative + while os.path.exists(project_dir): + UIFormatter.print_error(f"Directory '{project_dir}' already exists.") + UIFormatter.print_info("Please choose a different project directory name.") + project_dir = collector.get_validated_input( + "Enter project directory name", validate_project_name, "project directory name" + ) console.print() console.print(f"[{UIColors.HIGHLIGHT}]Django Project Name[/{UIColors.HIGHLIGHT}]") @@ -285,7 +305,7 @@ def get_user_input() -> Tuple[str, str, str, list, dict]: console.print() UIFormatter.print_separator() console.print(f"\n[{UIColors.INFO}]Step 2: Django Apps[/{UIColors.INFO}]\n") - + nested, nested_dir = collector.get_nested_apps_config() app_names = collector.get_app_names() console.print() @@ -293,7 +313,13 @@ def get_user_input() -> Tuple[str, str, str, list, dict]: use_github, use_gitlab = collector.get_cicd_choice() # Create metadata - metadata = ProjectMetadata(package_name=project_dir, use_github_actions=use_github, use_gitlab_ci=use_gitlab) + metadata = ProjectMetadata( + package_name=project_dir, + use_github_actions=use_github, + use_gitlab_ci=use_gitlab, + nested_apps=nested, + nested_dir=nested_dir, + ) console.print() diff --git a/src/templates/project.py b/src/scripts/project.py similarity index 69% rename from src/templates/project.py rename to src/scripts/project.py index 8be20fa..5714b1f 100644 --- a/src/templates/project.py +++ b/src/scripts/project.py @@ -12,20 +12,14 @@ from src.utils import change_cwd class ProjectManager: - def __init__(self, project_dir: str, project_name: str, app_names: list): + def __init__(self, project_dir: str, project_name: str, app_names: list, metadata: dict): self.project_dir = project_dir self.project_name = project_name self.app_names = app_names + self.metadata = metadata 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 @@ -41,7 +35,20 @@ class ProjectManager: return True def create_apps(self) -> bool: - with change_cwd(self.project_root): + # Determine base directory for apps + apps_base_dir = self.project_root + nested = bool(self.metadata.get("nested_apps")) + nested_dir_name = self.metadata.get("nested_dir") if nested else None + + if nested and nested_dir_name: + apps_base_dir = os.path.join(self.project_root, nested_dir_name) + os.makedirs(apps_base_dir, exist_ok=True) + # Make it a Python package + init_file = os.path.join(apps_base_dir, "__init__.py") + if not os.path.exists(init_file): + open(init_file, "a").close() + + with change_cwd(apps_base_dir): # Create each app for app_name in self.app_names: try: @@ -65,12 +72,12 @@ class ProjectManager: try: import django except ImportError: - UIFormatter.print_info("Django not found, installing...") subprocess.run( - [sys.executable, "-m", "pip", "install", "--upgrade", "django"], + [sys.executable, "-m", "pip", "install", "--upgrade", "django", "-q"], check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, ) - UIFormatter.print_success("Django installed successfully!") def validate_project_structure(self) -> bool: required_files = [ @@ -83,13 +90,17 @@ class ProjectManager: ] # Add required files for each app + 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(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"), + 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, "admin.py"), ] required_files.extend(app_files) diff --git a/src/scripts/settings.py b/src/scripts/settings.py new file mode 100644 index 0000000..8ef45ea --- /dev/null +++ b/src/scripts/settings.py @@ -0,0 +1,82 @@ +""" +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.scripts.template_engine import template_engine +from src.utils import change_cwd, create_file_with_content + + +class SettingsManager: + def __init__(self, project_root: str, project_name: str, app_names: list, metadata: dict): + self.project_root = project_root + self.project_name = project_name + self.app_names = app_names + self.metadata = metadata + 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): + nested = bool(self.metadata.get("nested_apps")) + nested_dir = self.metadata.get("nested_dir") + effective_app_names = [f"{nested_dir}.{name}" if nested and nested_dir else name for name in self.app_names] + context = { + "project_name": self.project_name, + "app_names": effective_app_names, + } + base_content = template_engine.render_template("settings_base.j2", context) + 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): + context = {"secret_key": self.secret_key} + dev_content = template_engine.render_template("settings_development.j2", context) + 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 = template_engine.render_template("settings_production.j2", {}) + return create_file_with_content( + "production.py", + prod_content, + "Updated settings/production.py", + should_format=True, + ) diff --git a/src/scripts/template_engine.py b/src/scripts/template_engine.py new file mode 100644 index 0000000..c4d491f --- /dev/null +++ b/src/scripts/template_engine.py @@ -0,0 +1,89 @@ +""" +Template engine for djinit. +Handles loading and rendering Jinja2 templates for file generation. +""" + +import os +from pathlib import Path +from typing import Any, Dict + +from jinja2 import Environment, FileSystemLoader, Template + + +class TemplateEngine: + """Template engine for rendering file templates.""" + + def __init__(self, template_dir: str = None): + """Initialize template engine. + + Args: + template_dir: Path to templates directory. If None, uses default. + """ + if template_dir is None: + template_dir = Path(__file__).resolve().parent.parent / "templates" + + if not template_dir.exists(): + raise FileNotFoundError(f"Template directory not found: {template_dir}") + + self.template_dir = str(template_dir) + self.env = Environment( + loader=FileSystemLoader(template_dir), trim_blocks=True, lstrip_blocks=True, keep_trailing_newline=True + ) + + def render_template(self, template_name: str, context: Dict[str, Any]) -> str: + """Render a template with the given context. + + Args: + template_name: Name of the template file (e.g., 'gitignore.j2') + context: Dictionary of variables to pass to the template + + Returns: + Rendered template content as string + """ + template = self.env.get_template(template_name) + return template.render(**context) + + def render_string(self, template_string: str, context: Dict[str, Any]) -> str: + """Render a template string with the given context. + + Args: + template_string: Template content as string + context: Dictionary of variables to pass to the template + + Returns: + Rendered template content as string + """ + template = Template(template_string, trim_blocks=True, lstrip_blocks=True) + return template.render(**context) + + def get_template_names(self) -> list[str]: + """Get list of available template files. + + Returns: + List of template file names + """ + if not os.path.exists(self.template_dir): + return [] + + template_files = [] + for file in os.listdir(self.template_dir): + if file.endswith(".j2"): + template_files.append(file) + + return sorted(template_files) + + def template_exists(self, template_name: str) -> bool: + """Check if a template file exists. + + Args: + template_name: Name of the template file + + Returns: + True if template exists, False otherwise + """ + template_path = os.path.join(self.template_dir, template_name) + return os.path.exists(template_path) + + +# Global template engine instance +template_engine = TemplateEngine() diff --git a/src/templates/__init__.py b/src/templates/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/templates/app_urls.j2 b/src/templates/app_urls.j2 new file mode 100644 index 0000000..a31f082 --- /dev/null +++ b/src/templates/app_urls.j2 @@ -0,0 +1,9 @@ +from django.urls import path +from . import views + +app_name = '{{ app_name }}' + +urlpatterns = [ + # Add your URL patterns here + # path('', views.index, name='index'), +] diff --git a/src/templates/asgi.j2 b/src/templates/asgi.j2 new file mode 100644 index 0000000..6380cd7 --- /dev/null +++ b/src/templates/asgi.j2 @@ -0,0 +1,9 @@ +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/src/templates/env_sample.j2 b/src/templates/env_sample.j2 new file mode 100644 index 0000000..0cc6e90 --- /dev/null +++ b/src/templates/env_sample.j2 @@ -0,0 +1,11 @@ +# Django settings +DJANGO_SETTINGS_MODULE={{ 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= diff --git a/src/templates/files.py b/src/templates/files.py deleted file mode 100644 index b389ee7..0000000 --- a/src/templates/files.py +++ /dev/null @@ -1,412 +0,0 @@ -""" -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/github_actions_ci.j2 b/src/templates/github_actions_ci.j2 new file mode 100644 index 0000000..52082de --- /dev/null +++ b/src/templates/github_actions_ci.j2 @@ -0,0 +1,109 @@ +name: Django CI + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main, develop ] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13"] + + services: + postgres: + image: postgres:15 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: test_db + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install uv + uses: astral-sh/setup-uv@v4 + with: + version: "latest" + + - name: Install dependencies + run: uv sync + + - name: Run linting + run: uv run ruff check . + + - name: Format check + run: uv run ruff format --check . + + - name: Set up PostgreSQL + env: + DB_NAME: test_db + DB_USER: postgres + DB_PASSWORD: postgres + DB_HOST: localhost + DB_PORT: 5432 + run: | + echo "PostgreSQL is ready" + + - name: Run Django tests + env: + DJANGO_SETTINGS_MODULE: {{ project_name }}.settings.development + SECRET_KEY: test-secret-key-for-ci + ALLOWED_HOSTS: localhost + DB_NAME: test_db + DB_USER: postgres + DB_PASSWORD: postgres + DB_HOST: localhost + DB_PORT: 5432 + run: | + uv run python manage.py check + uv run python manage.py test + + - name: Check migrations + env: + DJANGO_SETTINGS_MODULE: {{ project_name }}.settings.development + SECRET_KEY: test-secret-key-for-ci + ALLOWED_HOSTS: localhost + DB_NAME: test_db + DB_USER: postgres + DB_PASSWORD: postgres + DB_HOST: localhost + DB_PORT: 5432 + run: | + uv run python manage.py makemigrations --check --dry-run + + security: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install uv + uses: astral-sh/setup-uv@v4 + with: + version: "latest" + + - name: Install dependencies + run: uv sync + + - name: Run security check + run: uv run python manage.py check --deploy diff --git a/src/templates/gitignore.j2 b/src/templates/gitignore.j2 new file mode 100644 index 0000000..51c5a35 --- /dev/null +++ b/src/templates/gitignore.j2 @@ -0,0 +1,89 @@ +# 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/src/templates/gitlab_ci.j2 b/src/templates/gitlab_ci.j2 new file mode 100644 index 0000000..4fcedd6 --- /dev/null +++ b/src/templates/gitlab_ci.j2 @@ -0,0 +1,133 @@ +# GitLab CI/CD configuration for {{ project_name }} + +stages: + - test + - lint + - security + +variables: + POSTGRES_DB: test_db + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/test_db" + +# Use Docker-in-Docker +services: + - postgres:15 + +before_script: + - python --version + - pip install uv + - uv sync + +# Lint stage +lint: + stage: lint + image: python:3.13 + script: + - uv run ruff check . + - uv run ruff format --check . + only: + - branches + - merge_requests + +# Test stage +test: + stage: test + image: python:3.13 + variables: + DJANGO_SETTINGS_MODULE: {{ project_name }}.settings.development + SECRET_KEY: test-secret-key-for-ci + ALLOWED_HOSTS: localhost + script: + - uv run python manage.py check + - uv run python manage.py migrate + - uv run python manage.py test + only: + - branches + - merge_requests + +# Multiple Python versions +test:python310: + stage: test + image: python:3.10 + variables: + DJANGO_SETTINGS_MODULE: {{ project_name }}.settings.development + SECRET_KEY: test-secret-key-for-ci + ALLOWED_HOSTS: localhost + script: + - pip install uv + - uv sync + - uv run python manage.py check + - uv run python manage.py migrate + - uv run python manage.py test + only: + - branches + - merge_requests + +test:python311: + stage: test + image: python:3.11 + variables: + DJANGO_SETTINGS_MODULE: {{ project_name }}.settings.development + SECRET_KEY: test-secret-key-for-ci + ALLOWED_HOSTS: localhost + script: + - pip install uv + - uv sync + - uv run python manage.py check + - uv run python manage.py migrate + - uv run python manage.py test + only: + - branches + - merge_requests + +test:python312: + stage: test + image: python:3.12 + variables: + DJANGO_SETTINGS_MODULE: {{ project_name }}.settings.development + SECRET_KEY: test-secret-key-for-ci + ALLOWED_HOSTS: localhost + script: + - pip install uv + - uv sync + - uv run python manage.py check + - uv run python manage.py migrate + - uv run python manage.py test + only: + - branches + - merge_requests + +test:python313: + stage: test + image: python:3.13 + variables: + DJANGO_SETTINGS_MODULE: {{ project_name }}.settings.development + SECRET_KEY: test-secret-key-for-ci + ALLOWED_HOSTS: localhost + script: + - pip install uv + - uv sync + - uv run python manage.py check + - uv run python manage.py migrate + - uv run python manage.py test + only: + - branches + - merge_requests + +# Security check +security: + stage: security + image: python:3.13 + variables: + DJANGO_SETTINGS_MODULE: {{ project_name }}.settings.development + SECRET_KEY: test-secret-key-for-ci + script: + - pip install uv + - uv sync + - uv run python manage.py check --deploy + - uv run python manage.py validate_templates + only: + - main + - develop diff --git a/src/templates/justfile.j2 b/src/templates/justfile.j2 new file mode 100644 index 0000000..93cde70 --- /dev/null +++ b/src/templates/justfile.j2 @@ -0,0 +1,154 @@ +# Justfile for {{ project_name }} +# 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 {{ project_name }}.wsgi:application --bind 0.0.0.0:8000 + +# Start production server with gunicorn workers +server-prod WORKERS="4": + uv run gunicorn {{ project_name }}.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 {{ project_name }}.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/src/templates/manage_py.j2 b/src/templates/manage_py.j2 new file mode 100644 index 0000000..a616ada --- /dev/null +++ b/src/templates/manage_py.j2 @@ -0,0 +1,25 @@ +#!/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() diff --git a/src/templates/procfile.j2 b/src/templates/procfile.j2 new file mode 100644 index 0000000..bb2c0db --- /dev/null +++ b/src/templates/procfile.j2 @@ -0,0 +1,20 @@ +# Procfile for {{ project_name }} +# 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 {{ project_name }}.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 {{ project_name }}.wsgi:application --bind 0.0.0.0:$PORT --workers 4 --threads 2 --timeout 120 +# web: gunicorn {{ project_name }}.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 {{ project_name }} worker --loglevel=info +# beat: celery -A {{ project_name }} beat --loglevel=info + +# Development server (for local testing with Foreman/Overmind) +# web: python manage.py runserver 0.0.0.0:$PORT diff --git a/src/templates/project_urls.j2 b/src/templates/project_urls.j2 new file mode 100644 index 0000000..7395a4a --- /dev/null +++ b/src/templates/project_urls.j2 @@ -0,0 +1,29 @@ +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 +{% for app in app_names %} path("", include("{{ app }}.urls")), +{% endfor %}] + +# 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/src/templates/pyproject_toml.j2 b/src/templates/pyproject_toml.j2 new file mode 100644 index 0000000..cc4bde1 --- /dev/null +++ b/src/templates/pyproject_toml.j2 @@ -0,0 +1,41 @@ +[project] +name = "{{ 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 = ["{{ project_name }}"] diff --git a/src/templates/readme.j2 b/src/templates/readme.j2 new file mode 100644 index 0000000..956aa49 --- /dev/null +++ b/src/templates/readme.j2 @@ -0,0 +1,60 @@ +# {{ 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 + +``` +{{ project_name }}/ +├── {{ project_name }}/ +│ ├── settings/ +│ │ ├── base.py +│ │ ├── development.py +│ │ └── production.py +│ ├── urls.py +│ ├── wsgi.py +│ └── asgi.py +{% for app in app_names %}├── {{ app }}/ +{% endfor %}├── 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/ diff --git a/src/templates/requirements.j2 b/src/templates/requirements.j2 new file mode 100644 index 0000000..6bdaa52 --- /dev/null +++ b/src/templates/requirements.j2 @@ -0,0 +1,10 @@ +Django +python-dotenv +django-jazzmin> +djangorestframework +djangorestframework_simplejwt +drf-spectacular +django-cors-headers +whitenoise +psycopg2-binary +gunicorn diff --git a/src/templates/runtime_txt.j2 b/src/templates/runtime_txt.j2 new file mode 100644 index 0000000..7cfeb51 --- /dev/null +++ b/src/templates/runtime_txt.j2 @@ -0,0 +1 @@ +python-{{ python_version }} diff --git a/src/templates/settings.py b/src/templates/settings.py deleted file mode 100644 index b56d3a4..0000000 --- a/src/templates/settings.py +++ /dev/null @@ -1,309 +0,0 @@ -""" -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, - ) diff --git a/src/templates/settings_base.j2 b/src/templates/settings_base.j2 new file mode 100644 index 0000000..63cc238 --- /dev/null +++ b/src/templates/settings_base.j2 @@ -0,0 +1,161 @@ +""" +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 = [ +{% for app in app_names %} "{{ app }}", +{% endfor %}] + +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 = "{{ 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 = "{{ 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": "{{ project_name }} API", + "DESCRIPTION": "API documentation for {{ 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" diff --git a/src/templates/settings_development.j2 b/src/templates/settings_development.j2 new file mode 100644 index 0000000..cb5aa6e --- /dev/null +++ b/src/templates/settings_development.j2 @@ -0,0 +1,27 @@ +from .base import * + +SECRET_KEY = "{{ 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 diff --git a/src/templates/settings_production.j2 b/src/templates/settings_production.j2 new file mode 100644 index 0000000..4177d81 --- /dev/null +++ b/src/templates/settings_production.j2 @@ -0,0 +1,40 @@ +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' diff --git a/src/templates/wsgi.j2 b/src/templates/wsgi.j2 new file mode 100644 index 0000000..d414bb0 --- /dev/null +++ b/src/templates/wsgi.j2 @@ -0,0 +1,9 @@ +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/src/utils.py b/src/utils.py index 20ba67e..71056c0 100644 --- a/src/utils.py +++ b/src/utils.py @@ -15,9 +15,7 @@ def format_file(filename: str) -> None: subprocess.run(["ruff", "format", filename], check=False, capture_output=True) -def create_file_with_content( - filename: str, content: str, success_message: str, should_format: bool = False -) -> bool: +def create_file_with_content(filename: str, content: str, success_message: str, should_format: bool = False) -> bool: """ Create a file with content, optionally format it, and print success message.