refactor: improve project creation and app generation for better compatibility
This commit is contained in:
41 files changed
+239
-810
No files matched your search
@@ -19,7 +19,12 @@ class Cli:
|
||||
self.primary_app = primary_app
|
||||
self.app_names = app_names
|
||||
self.metadata = metadata
|
||||
<<<<<<< HEAD
|
||||
if project_dir == ".": # handle '.' for current directory
|
||||
=======
|
||||
# Handle '.' for current directory
|
||||
if project_dir == ".":
|
||||
>>>>>>> origin/main
|
||||
self.project_root = os.getcwd()
|
||||
else:
|
||||
self.project_root = os.path.join(os.getcwd(), project_dir)
|
||||
|
||||
@@ -4,6 +4,11 @@ Handles creation of Django apps and updating settings.
|
||||
"""
|
||||
|
||||
import os
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
import subprocess
|
||||
import sys
|
||||
>>>>>>> origin/main
|
||||
from typing import Optional
|
||||
|
||||
from djinit.scripts.console_ui import UIFormatter
|
||||
@@ -70,9 +75,18 @@ class AppManager:
|
||||
UIFormatter.print_error("Could not find manage.py file in project root")
|
||||
return False
|
||||
|
||||
<<<<<<< HEAD
|
||||
# If predefined structure detected (apps/ exists), scaffold nested layout using custom templates
|
||||
if self._is_predefined_structure():
|
||||
return self._create_predefined_app(os.path.join(self.current_dir, "apps"))
|
||||
=======
|
||||
# Create app in the correct location
|
||||
# Use 'python -m django' instead of 'django-admin' for better compatibility
|
||||
with change_cwd(apps_base_dir):
|
||||
subprocess.run(
|
||||
[sys.executable, "-m", "django", "startapp", self.app_name], capture_output=True, text=True, check=True
|
||||
)
|
||||
>>>>>>> origin/main
|
||||
|
||||
# Otherwise, use standard flat app generation
|
||||
success = DjangoHelper.startapp(self.app_name, apps_base_dir)
|
||||
|
||||
@@ -204,6 +204,10 @@ class InputCollector:
|
||||
console.print(f" [{UIColors.SUCCESS}]{option.key}[/{UIColors.SUCCESS}] {option.description}")
|
||||
console.print()
|
||||
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
# Get valid keys from CICDOption enum
|
||||
>>>>>>> origin/main
|
||||
valid_keys = [option.key for option in CICDOption]
|
||||
|
||||
choice = CharReader.get_cicd_choice(
|
||||
@@ -212,12 +216,24 @@ class InputCollector:
|
||||
default=CICDOption.NONE.key,
|
||||
)
|
||||
|
||||
<<<<<<< HEAD
|
||||
choice_map = {
|
||||
CICDOption.BOTH.key: (True, True),
|
||||
CICDOption.GITHUB.key: (True, False),
|
||||
CICDOption.GITLAB.key: (False, True),
|
||||
}
|
||||
return choice_map.get(choice, (False, False))
|
||||
=======
|
||||
# Parse choice
|
||||
if choice == CICDOption.BOTH.key:
|
||||
return True, True
|
||||
elif choice == CICDOption.GITHUB.key:
|
||||
return True, False
|
||||
elif choice == CICDOption.GITLAB.key:
|
||||
return False, True
|
||||
else: # Default to none (should not happen due to validation, but kept as fallback)
|
||||
return False, False
|
||||
>>>>>>> origin/main
|
||||
|
||||
def get_nested_apps_config(self) -> Tuple[bool, str | None]:
|
||||
UIFormatter.print_separator()
|
||||
@@ -226,7 +242,13 @@ class InputCollector:
|
||||
)
|
||||
console.print()
|
||||
|
||||
<<<<<<< HEAD
|
||||
choice = CharReader.get_yes_no(f"[{UIColors.HIGHLIGHT}]Nested Django apps? (y/N):[/{UIColors.HIGHLIGHT}]")
|
||||
=======
|
||||
choice = CharReader.get_yes_no(
|
||||
f"[{UIColors.HIGHLIGHT}]Nested Django apps? (y/N):[/{UIColors.HIGHLIGHT}]", default="n"
|
||||
)
|
||||
>>>>>>> origin/main
|
||||
|
||||
if choice != "y":
|
||||
return False, None
|
||||
@@ -246,7 +268,16 @@ class InputCollector:
|
||||
console.print(f" [{UIColors.SUCCESS}]N[/{UIColors.SUCCESS}] Use individual database parameters")
|
||||
console.print()
|
||||
|
||||
<<<<<<< HEAD
|
||||
return CharReader.get_yes_no(f"[{UIColors.HIGHLIGHT}]Use DATABASE_URL? (Y/n):[/{UIColors.HIGHLIGHT}]") == "y"
|
||||
=======
|
||||
choice = CharReader.get_yes_no(
|
||||
f"[{UIColors.HIGHLIGHT}]Use DATABASE_URL? (Y/n):[/{UIColors.HIGHLIGHT}]", default="y"
|
||||
)
|
||||
|
||||
# Return True if choice is 'y', False if 'n'
|
||||
return choice == "y"
|
||||
>>>>>>> origin/main
|
||||
|
||||
|
||||
class CharReader:
|
||||
@@ -259,6 +290,7 @@ class CharReader:
|
||||
return CharReader._get_char_unix()
|
||||
|
||||
@staticmethod
|
||||
<<<<<<< HEAD
|
||||
def _is_enter_key(response: str) -> bool:
|
||||
"""Check if response represents Enter key or control character."""
|
||||
return not response or response in ("\r", "\n") or ord(response) < 32
|
||||
@@ -291,10 +323,30 @@ class CharReader:
|
||||
# Handle Enter key (default)
|
||||
if CharReader._is_enter_key(response):
|
||||
return CharReader._handle_default(default)
|
||||
=======
|
||||
def get_yes_no(prompt: str, default: str = "n") -> str:
|
||||
max_attempts = 3
|
||||
attempt = 0
|
||||
|
||||
while attempt < max_attempts:
|
||||
try:
|
||||
console.print(f"{prompt} ", end="")
|
||||
sys.stdout.flush()
|
||||
|
||||
reader = CharReader()
|
||||
response = reader.get_char()
|
||||
|
||||
# Handle Enter key (default) - in raw mode, Enter is '\r' (carriage return)
|
||||
# Also handle empty string or other control characters as Enter
|
||||
if not response or response == "\r" or response == "\n" or ord(response) < 32:
|
||||
console.print(default.upper())
|
||||
return default.lower()
|
||||
>>>>>>> origin/main
|
||||
|
||||
response = response.lower()
|
||||
|
||||
# Validate response
|
||||
<<<<<<< HEAD
|
||||
if response in valid_keys_lower:
|
||||
console.print(response.upper())
|
||||
return response
|
||||
@@ -307,11 +359,30 @@ class CharReader:
|
||||
if attempt < CharReader.MAX_ATTEMPTS:
|
||||
console.print(
|
||||
f"[{UIColors.MUTED}]Please try again ({attempt}/{CharReader.MAX_ATTEMPTS}).[/{UIColors.MUTED}]"
|
||||
=======
|
||||
if response in ("y", "n"):
|
||||
console.print(response.upper())
|
||||
return response
|
||||
|
||||
# Invalid input - show error (only show if it's a printable character)
|
||||
console.print()
|
||||
if response.isprintable():
|
||||
UIFormatter.print_error(f"Invalid input '{response}'. Please enter 'y' or 'n'.")
|
||||
attempt += 1
|
||||
if attempt < max_attempts:
|
||||
console.print(
|
||||
f"[{UIColors.MUTED}]Please try again ({attempt}/{max_attempts}).[/{UIColors.MUTED}]"
|
||||
>>>>>>> origin/main
|
||||
)
|
||||
console.print()
|
||||
else:
|
||||
# Control character entered, treat as Enter
|
||||
<<<<<<< HEAD
|
||||
return CharReader._handle_default(default)
|
||||
=======
|
||||
console.print(default.upper())
|
||||
return default.lower()
|
||||
>>>>>>> origin/main
|
||||
|
||||
except KeyboardInterrupt:
|
||||
console.print()
|
||||
@@ -324,6 +395,7 @@ class CharReader:
|
||||
return default.lower()
|
||||
|
||||
@staticmethod
|
||||
<<<<<<< HEAD
|
||||
def get_yes_no(prompt: str, default: str = None) -> str:
|
||||
# Auto-detect default from prompt format (capital letter indicates default)
|
||||
if default is None:
|
||||
@@ -351,6 +423,70 @@ class CharReader:
|
||||
default=default,
|
||||
error_message=f"Invalid input '{{response}}'. Please enter one of: {valid_keys_str}.",
|
||||
)
|
||||
=======
|
||||
def get_cicd_choice(prompt: str, valid_keys: list[str], default: str = "n") -> str:
|
||||
"""
|
||||
Get CI/CD choice input with validation and retries.
|
||||
|
||||
Args:
|
||||
prompt: The prompt message to display
|
||||
valid_keys: List of valid key characters (e.g., ['g', 'l', 'b', 'n'])
|
||||
default: Default value if Enter is pressed
|
||||
|
||||
Returns:
|
||||
Valid key (lowercase)
|
||||
"""
|
||||
max_attempts = 3
|
||||
attempt = 0
|
||||
valid_keys_lower = [k.lower() for k in valid_keys]
|
||||
|
||||
while attempt < max_attempts:
|
||||
try:
|
||||
console.print(f"{prompt} ", end="")
|
||||
sys.stdout.flush()
|
||||
|
||||
reader = CharReader()
|
||||
response = reader.get_char()
|
||||
|
||||
# Handle Enter key (default) - in raw mode, Enter is '\r' (carriage return)
|
||||
# Also handle empty string or other control characters as Enter
|
||||
if not response or response == "\r" or response == "\n" or ord(response) < 32:
|
||||
console.print(default.upper())
|
||||
return default.lower()
|
||||
|
||||
response = response.lower()
|
||||
|
||||
# Validate response
|
||||
if response in valid_keys_lower:
|
||||
console.print(response.upper())
|
||||
return response
|
||||
|
||||
# Invalid input - show error (only show if it's a printable character)
|
||||
console.print()
|
||||
if response.isprintable():
|
||||
valid_keys_str = ", ".join(f"'{k}'" for k in valid_keys)
|
||||
UIFormatter.print_error(f"Invalid input '{response}'. Please enter one of: {valid_keys_str}.")
|
||||
attempt += 1
|
||||
if attempt < max_attempts:
|
||||
console.print(
|
||||
f"[{UIColors.MUTED}]Please try again ({attempt}/{max_attempts}).[/{UIColors.MUTED}]"
|
||||
)
|
||||
console.print()
|
||||
else:
|
||||
# Control character entered, treat as Enter
|
||||
console.print(default.upper())
|
||||
return default.lower()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
console.print()
|
||||
UIFormatter.print_info("\nOperation cancelled by user.")
|
||||
raise
|
||||
|
||||
# Max attempts reached - use default
|
||||
console.print()
|
||||
UIFormatter.print_warning(f"Maximum attempts reached. Using default: '{default.upper()}'.")
|
||||
return default.lower()
|
||||
>>>>>>> origin/main
|
||||
|
||||
@staticmethod
|
||||
def _get_char_windows() -> str:
|
||||
@@ -389,10 +525,64 @@ def get_user_input() -> Tuple[str, str, str, list, dict]:
|
||||
|
||||
# Pre-step: Ask for structure type first
|
||||
UIFormatter.print_separator()
|
||||
<<<<<<< HEAD
|
||||
console.print(f"\n[{UIColors.INFO}]Choose Structure Type[/{UIColors.INFO}]\n")
|
||||
console.print(f"[{UIColors.MUTED}]1. Standard structure (default Django layout)[/{UIColors.MUTED}]")
|
||||
console.print(f"[{UIColors.MUTED}]2. Predefined structure (apps/users, apps/core, api/)[/{UIColors.MUTED}]")
|
||||
console.print(f"[{UIColors.MUTED}]3. Unified structure (core/, apps/core, apps/api)[/{UIColors.MUTED}]")
|
||||
=======
|
||||
console.print(f"\n[{UIColors.INFO}]Step 1: Project Setup[/{UIColors.INFO}]\n")
|
||||
|
||||
console.print(f"[{UIColors.HIGHLIGHT}]Project Directory[/{UIColors.HIGHLIGHT}]")
|
||||
console.print(f"[{UIColors.MUTED}]Where your project files will be created[/{UIColors.MUTED}]")
|
||||
console.print(f"[{UIColors.MUTED}]Press Enter or enter '.' to create in current directory[/{UIColors.MUTED}]")
|
||||
|
||||
# Get project directory with special handling for '.' and empty input
|
||||
attempt = 0
|
||||
project_dir = None
|
||||
|
||||
while attempt < collector.MAX_ATTEMPTS and project_dir is None:
|
||||
try:
|
||||
user_input = console.input(
|
||||
f"[{UIColors.HIGHLIGHT}]Enter project directory name:[/{UIColors.HIGHLIGHT}] "
|
||||
).strip()
|
||||
|
||||
# Handle empty input or '.' - use current directory
|
||||
if not user_input or user_input == ".":
|
||||
project_dir = "."
|
||||
UIFormatter.print_info(f"Creating project in current directory: {os.getcwd()}")
|
||||
break
|
||||
|
||||
# Validate the input
|
||||
is_valid, error_msg = validate_project_name(user_input)
|
||||
if not is_valid:
|
||||
UIFormatter.print_error(error_msg)
|
||||
attempt += 1
|
||||
collector._show_retry_message(attempt)
|
||||
continue
|
||||
|
||||
# Check if directory exists and ask for alternative
|
||||
if os.path.exists(user_input):
|
||||
UIFormatter.print_error(f"Directory '{user_input}' already exists.")
|
||||
UIFormatter.print_info(
|
||||
"Please choose a different project directory name (or press Enter for current directory)."
|
||||
)
|
||||
attempt += 1
|
||||
collector._show_retry_message(attempt)
|
||||
continue
|
||||
|
||||
project_dir = user_input
|
||||
break
|
||||
|
||||
except KeyboardInterrupt:
|
||||
UIFormatter.print_info("\nSetup cancelled by user.")
|
||||
sys.exit(0)
|
||||
|
||||
if project_dir is None:
|
||||
UIFormatter.print_error("Maximum attempts reached for project directory name. Exiting.")
|
||||
sys.exit(1)
|
||||
|
||||
>>>>>>> origin/main
|
||||
console.print()
|
||||
|
||||
structure_choice = console.input(
|
||||
@@ -544,7 +734,13 @@ def confirm_setup(project_dir: str, project_name: str, app_names: list, metadata
|
||||
console.print()
|
||||
|
||||
try:
|
||||
<<<<<<< HEAD
|
||||
response = CharReader.get_yes_no(f"[{UIColors.WARNING}]Proceed with setup? (y/N):[/{UIColors.WARNING}]")
|
||||
=======
|
||||
response = CharReader.get_yes_no(
|
||||
f"[{UIColors.WARNING}]Proceed with setup? (y/N):[/{UIColors.WARNING}]", default="n"
|
||||
)
|
||||
>>>>>>> origin/main
|
||||
return response == "y"
|
||||
|
||||
except KeyboardInterrupt:
|
||||
|
||||
@@ -22,14 +22,23 @@ class ProjectManager:
|
||||
self.project_name = project_name
|
||||
self.app_names = app_names
|
||||
self.metadata = metadata
|
||||
<<<<<<< HEAD
|
||||
self.project_root = os.getcwd() if project_dir == "." else os.path.join(os.getcwd(), project_dir)
|
||||
# Support custom module name for the Django config package (e.g., 'config')
|
||||
# Fall back to project_name if key present but None
|
||||
self.module_name = metadata.get("project_module_name") or self.project_name
|
||||
=======
|
||||
# Handle '.' for current directory
|
||||
if project_dir == ".":
|
||||
self.project_root = os.getcwd()
|
||||
else:
|
||||
self.project_root = os.path.join(os.getcwd(), project_dir)
|
||||
>>>>>>> origin/main
|
||||
|
||||
def create_project(self) -> bool:
|
||||
os.makedirs(self.project_root, exist_ok=True)
|
||||
|
||||
<<<<<<< HEAD
|
||||
unified = self.metadata.get("unified_structure", False)
|
||||
success = DjangoHelper.startproject(self.module_name, self.project_root, unified=unified)
|
||||
if success:
|
||||
@@ -37,6 +46,17 @@ class ProjectManager:
|
||||
else:
|
||||
UIFormatter.print_error(f"Failed to create Django project '{self.project_name}'")
|
||||
return success
|
||||
=======
|
||||
# Create Django project inside the directory
|
||||
# Use 'python -m django' instead of 'django-admin' for better compatibility
|
||||
subprocess.run(
|
||||
[sys.executable, "-m", "django", "startproject", self.project_name, self.project_root],
|
||||
check=True,
|
||||
)
|
||||
|
||||
UIFormatter.print_success(f"Django project '{self.project_name}' created successfully!")
|
||||
return True
|
||||
>>>>>>> origin/main
|
||||
|
||||
def create_apps(self) -> bool:
|
||||
apps_base_dir = self.project_root
|
||||
|
||||
@@ -14,6 +14,10 @@ from djinit.scripts.template_engine import template_engine
|
||||
|
||||
def format_file(filename: str) -> None:
|
||||
"""Format Python file using Ruff formatter."""
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
# Use 'python -m ruff' instead of 'ruff' for better compatibility
|
||||
>>>>>>> origin/main
|
||||
subprocess.run([sys.executable, "-m", "ruff", "format", filename], check=False, capture_output=True)
|
||||
|
||||
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
# Django settings
|
||||
DJANGO_SETTINGS_MODULE=yoo.settings.development
|
||||
SECRET_KEY= your-secret-key
|
||||
ALLOWED_HOSTS=api.your-domain.com,www.your-domain.com
|
||||
|
||||
# Database (individual parameters)
|
||||
DB_NAME=
|
||||
DB_USER=
|
||||
DB_PASSWORD=
|
||||
DB_HOST=
|
||||
DB_PORT=
|
||||
|
||||
# Email settings
|
||||
EMAIL_HOST=
|
||||
EMAIL_PORT=
|
||||
EMAIL_USE_TLS=
|
||||
EMAIL_HOST_USER=
|
||||
EMAIL_HOST_PASSWORD=
|
||||
DEFAULT_FROM_EMAIL=
|
||||
SERVER_EMAIL=
|
||||
@@ -1,93 +0,0 @@
|
||||
#--------------------------------------------------#
|
||||
# The following was generated by djinit: #
|
||||
#--------------------------------------------------#
|
||||
|
||||
# Python
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
__pycache__/
|
||||
*.egg-info/
|
||||
dist/
|
||||
build/
|
||||
.eggs/
|
||||
*.egg
|
||||
|
||||
# Django
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
*.sqlite3
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# IDEs
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.project
|
||||
.pydevproject
|
||||
.settings/
|
||||
.ropeproject/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
*.DS_Store
|
||||
Thumbs.db
|
||||
desktop.ini
|
||||
|
||||
# Django specific
|
||||
media/
|
||||
staticfiles/
|
||||
staticfiles/
|
||||
static_root/
|
||||
/static/
|
||||
|
||||
# Testing
|
||||
.coverage
|
||||
htmlcov/
|
||||
.pytest_cache/
|
||||
.tox/
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
.cache
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Node modules (if using frontend tools)
|
||||
node_modules/
|
||||
|
||||
# Secret keys
|
||||
secret.txt
|
||||
secrets.json
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# Backup files
|
||||
*.bak
|
||||
*.tmp
|
||||
*~
|
||||
|
||||
# Database
|
||||
*.db
|
||||
*.sql
|
||||
|
||||
# Cloud
|
||||
.cloud
|
||||
@@ -1,20 +0,0 @@
|
||||
# Procfile for yoo
|
||||
# Used for deploying to PaaS platforms like Heroku, Railway, Render, etc.
|
||||
|
||||
# Web server process (using Gunicorn)
|
||||
# Adjust workers based on your needs: (2 x CPU cores) + 1
|
||||
web: gunicorn yoo.wsgi:application --bind 0.0.0.0:$PORT --workers 2 --timeout 120 --access-logfile - --error-logfile - --log-level info
|
||||
|
||||
# Alternative web server configurations (uncomment to use):
|
||||
# web: gunicorn yoo.wsgi:application --bind 0.0.0.0:$PORT --workers 4 --threads 2 --timeout 120
|
||||
# web: gunicorn yoo.wsgi:application --bind 0.0.0.0:$PORT --workers 2 --worker-class gevent --worker-connections 1000 --timeout 120
|
||||
|
||||
# Release task (runs migrations before deployment)
|
||||
release: python manage.py migrate --noinput && python manage.py collectstatic --noinput
|
||||
|
||||
# Uncomment below to run scheduled tasks (if using Celery)
|
||||
# worker: celery -A yoo worker --loglevel=info
|
||||
# beat: celery -A yoo beat --loglevel=info
|
||||
|
||||
# Development server (for local testing with Foreman/Overmind)
|
||||
# web: python manage.py runserver 0.0.0.0:$PORT
|
||||
@@ -1,84 +0,0 @@
|
||||
# yoo
|
||||
|
||||
A Django project created with django-setup.
|
||||
|
||||
## Features
|
||||
|
||||
- Modern project structure with environment-specific settings
|
||||
- Pre-configured REST API with JWT authentication
|
||||
- Essential dependencies and utilities
|
||||
- Production-ready configuration
|
||||
|
||||
## Setup
|
||||
|
||||
1. Install dependencies:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
2. Set environment variables in `.env` file
|
||||
|
||||
3. Run migrations:
|
||||
```bash
|
||||
python manage.py migrate
|
||||
```
|
||||
|
||||
4. Create superuser:
|
||||
```bash
|
||||
python manage.py createsuperuser
|
||||
```
|
||||
|
||||
5. Start development server:
|
||||
```bash
|
||||
python manage.py runserver
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
yoo/
|
||||
├── core/
|
||||
│ ├── settings/
|
||||
│ │ ├── base.py
|
||||
│ │ ├── development.py
|
||||
│ │ └── production.py
|
||||
│ ├── urls.py
|
||||
│ ├── wsgi.py
|
||||
│ └── asgi.py
|
||||
├── apps/
|
||||
│ ├── core/
|
||||
│ │ ├── apps.py
|
||||
│ │ ├── models/
|
||||
│ │ │ ├── __init__.py
|
||||
│ │ │ ├── base.py
|
||||
│ │ │ └── mixins.py
|
||||
│ │ ├── utils/
|
||||
│ │ │ ├── __init__.py
|
||||
│ │ │ └── responses.py
|
||||
│ │ ├── permissions/
|
||||
│ │ ├── middleware/
|
||||
│ │ └── __init__.py
|
||||
│ └── api/
|
||||
│ ├── apps.py
|
||||
│ ├── models/
|
||||
│ ├── serializers/
|
||||
│ ├── views/
|
||||
│ ├── services/
|
||||
│ ├── tests/
|
||||
│ ├── urls/
|
||||
│ └── admin/
|
||||
├── manage.py
|
||||
├── requirements.txt
|
||||
├── pyproject.toml
|
||||
├── .env.sample
|
||||
├── justfile
|
||||
├── Procfile
|
||||
├── runtime.txt
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## API Documentation
|
||||
|
||||
When running in development mode, API documentation is available at:
|
||||
- Swagger UI: http://localhost:8000/docs/
|
||||
- Schema: http://localhost:8000/schema/
|
||||
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
@@ -1,8 +0,0 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class Apps.apiConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.api'
|
||||
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
@@ -1,8 +0,0 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class Config(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = ""
|
||||
Whitespace-only changes.
@@ -1,5 +0,0 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
@@ -1,13 +0,0 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.db import models
|
||||
|
||||
|
||||
class AbstractBaseModel(models.Model):
|
||||
"""Abstract base model with common fields."""
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
@@ -1,13 +0,0 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.db import models
|
||||
|
||||
|
||||
class TimestampedModel(models.Model):
|
||||
"""Mixin for models that need timestamp fields."""
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
Whitespace-only changes.
@@ -1,3 +0,0 @@
|
||||
from .responses import success, error
|
||||
|
||||
__all__ = ["success", "error"]
|
||||
@@ -1,13 +0,0 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from rest_framework.response import Response
|
||||
|
||||
|
||||
def success(data=None, status_code=200):
|
||||
"""Return a successful response."""
|
||||
return Response({"success": True, "data": data}, status=status_code)
|
||||
|
||||
|
||||
def error(message, status_code=400):
|
||||
"""Return an error response."""
|
||||
return Response({"success": False, "message": message}, status=status_code)
|
||||
Whitespace-only changes.
@@ -1,11 +0,0 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
load_dotenv()
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", os.getenv("DJANGO_SETTINGS_MODULE"))
|
||||
|
||||
application = get_asgi_application()
|
||||
Whitespace-only changes.
@@ -1,163 +0,0 @@
|
||||
"""
|
||||
generated with djinit
|
||||
Common settings shared between development and production environment
|
||||
"""
|
||||
|
||||
import os
|
||||
from datetime import timedelta
|
||||
from pathlib import Path
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
|
||||
|
||||
|
||||
# Application definition
|
||||
THIRD_PARTY_APPS = [
|
||||
"rest_framework",
|
||||
"rest_framework_simplejwt",
|
||||
"rest_framework_simplejwt.token_blacklist",
|
||||
"corsheaders",
|
||||
"drf_spectacular",
|
||||
]
|
||||
|
||||
USER_DEFINED_APPS = [
|
||||
"apps.core",
|
||||
"apps.api",
|
||||
]
|
||||
|
||||
BUILT_IN_APPS = [
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
]
|
||||
|
||||
INSTALLED_APPS = BUILT_IN_APPS + THIRD_PARTY_APPS + USER_DEFINED_APPS
|
||||
INSTALLED_APPS.insert(0, "jazzmin")
|
||||
|
||||
MIDDLEWARE = [
|
||||
"corsheaders.middleware.CorsMiddleware",
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"whitenoise.middleware.WhiteNoiseMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
]
|
||||
|
||||
ROOT_URLCONF = "core.urls"
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": [],
|
||||
"APP_DIRS": True,
|
||||
"OPTIONS": {
|
||||
"context_processors": [
|
||||
"django.template.context_processors.request",
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = "core.wsgi.application"
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/5.2/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = "en-us"
|
||||
TIME_ZONE = "UTC"
|
||||
USE_I18N = True
|
||||
USE_TZ = True
|
||||
|
||||
# Static files and media files
|
||||
STATIC_URL = "static/"
|
||||
STATIC_ROOT = BASE_DIR / "staticfiles"
|
||||
|
||||
MEDIA_URL = "media/"
|
||||
MEDIA_ROOT = BASE_DIR / "media"
|
||||
|
||||
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
|
||||
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
|
||||
# REST Framework Settings
|
||||
REST_FRAMEWORK = {
|
||||
"DEFAULT_RENDERER_CLASSES": [
|
||||
"rest_framework.renderers.JSONRenderer",
|
||||
],
|
||||
"DEFAULT_PARSER_CLASSES": [
|
||||
"rest_framework.parsers.JSONParser",
|
||||
],
|
||||
"DEFAULT_AUTHENTICATION_CLASSES": (
|
||||
"rest_framework_simplejwt.authentication.JWTAuthentication",
|
||||
"rest_framework.authentication.SessionAuthentication",
|
||||
),
|
||||
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
|
||||
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
|
||||
"PAGE_SIZE": 20,
|
||||
}
|
||||
|
||||
# DRF Spectacular settings
|
||||
SPECTACULAR_SETTINGS = {
|
||||
"TITLE": "core API",
|
||||
"DESCRIPTION": "API documentation for core",
|
||||
"VERSION": "1.0.0",
|
||||
"SERVE_INCLUDE_SCHEMA": False,
|
||||
"COMPONENT_SPLIT_REQUEST": True,
|
||||
"SCHEMA_PATH_PREFIX": "/api/",
|
||||
}
|
||||
|
||||
# JWT Settings
|
||||
SIMPLE_JWT = {
|
||||
"ACCESS_TOKEN_LIFETIME": timedelta(days=1),
|
||||
"REFRESH_TOKEN_LIFETIME": timedelta(days=3),
|
||||
"ROTATE_REFRESH_TOKENS": True,
|
||||
"BLACKLIST_AFTER_ROTATION": True,
|
||||
}
|
||||
|
||||
# CORS Settings
|
||||
CORS_ALLOWED_ORIGINS = [
|
||||
"http://localhost:3000",
|
||||
"http://127.0.0.1:3000",
|
||||
]
|
||||
|
||||
CORS_ALLOW_CREDENTIALS = True
|
||||
|
||||
# Security Settings
|
||||
SECURE_BROWSER_XSS_FILTER = True
|
||||
SECURE_CONTENT_TYPE_NOSNIFF = True
|
||||
X_FRAME_OPTIONS = "DENY"
|
||||
@@ -1,29 +0,0 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from .base import *
|
||||
|
||||
SECRET_KEY = "4RxQW##Ss0h(8&Q3snx(2Km(qw75kRjGM#-zyV7*KUV&3)rUGZ"
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = ["*"]
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": BASE_DIR / "db.sqlite3",
|
||||
}
|
||||
}
|
||||
|
||||
# Email backend for development
|
||||
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
||||
|
||||
# CORS settings for development
|
||||
CORS_ALLOW_ALL_ORIGINS = True
|
||||
|
||||
# Disable HTTPS redirect in development
|
||||
SECURE_SSL_REDIRECT = False
|
||||
@@ -1,47 +0,0 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from .base import *
|
||||
import os
|
||||
import dj_database_url
|
||||
|
||||
DEBUG = False
|
||||
SECRET_KEY = os.getenv("SECRET_KEY")
|
||||
ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "").split(",")
|
||||
|
||||
# Database
|
||||
# Using individual database parameters
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.postgresql",
|
||||
"NAME": os.getenv("DB_NAME"),
|
||||
"USER": os.getenv("DB_USER"),
|
||||
"PASSWORD": os.getenv("DB_PASSWORD"),
|
||||
"HOST": os.getenv("DB_HOST"),
|
||||
"PORT": os.getenv("DB_PORT"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Email settings
|
||||
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
|
||||
EMAIL_HOST = os.getenv("EMAIL_HOST")
|
||||
EMAIL_PORT = int(os.getenv("EMAIL_PORT", 587))
|
||||
EMAIL_USE_TLS = os.getenv("EMAIL_USE_TLS", "True").lower() == "true"
|
||||
EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER")
|
||||
EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD")
|
||||
DEFAULT_FROM_EMAIL = os.environ.get("EMAIL_HOST_USER")
|
||||
SERVER_EMAIL = os.environ.get("EMAIL_HOST_USER")
|
||||
|
||||
# Security settings for production
|
||||
SECURE_SSL_REDIRECT = True
|
||||
SECURE_HSTS_SECONDS = 31536000
|
||||
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
||||
SECURE_HSTS_PRELOAD = True
|
||||
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
||||
|
||||
# Session settings
|
||||
SESSION_COOKIE_SECURE = True
|
||||
CSRF_COOKIE_SECURE = True
|
||||
|
||||
# Static files
|
||||
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
|
||||
@@ -1,30 +0,0 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from django.conf import settings
|
||||
from drf_spectacular.views import (
|
||||
SpectacularAPIView,
|
||||
SpectacularSwaggerView,
|
||||
)
|
||||
from rest_framework_simplejwt.views import (
|
||||
TokenObtainPairView,
|
||||
TokenRefreshView,
|
||||
TokenBlacklistView,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
# JWT tokens
|
||||
path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
|
||||
path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
|
||||
path("token/blacklist/", TokenBlacklistView.as_view(), name="token_blacklist"),
|
||||
path("api/", include("apps.api.urls")),
|
||||
]
|
||||
|
||||
# Don't show schema in production
|
||||
if settings.DEBUG:
|
||||
urlpatterns += [
|
||||
path("schema/", SpectacularAPIView.as_view(), name="schema"),
|
||||
path("docs/", SpectacularSwaggerView.as_view(url_name="schema")),
|
||||
]
|
||||
@@ -1,11 +0,0 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
load_dotenv()
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", os.getenv("DJANGO_SETTINGS_MODULE"))
|
||||
|
||||
application = get_wsgi_application()
|
||||
-154
@@ -1,154 +0,0 @@
|
||||
# Justfile for yoo
|
||||
# Task runner for Django development
|
||||
# Install just: https://github.com/casey/just
|
||||
|
||||
# Default task: show available commands
|
||||
default:
|
||||
@just --list
|
||||
|
||||
# Run development server
|
||||
dev:
|
||||
uv run python manage.py runserver
|
||||
|
||||
# Run development server with custom host and port
|
||||
dev-server HOST="0.0.0.0" PORT="8000":
|
||||
uv run python manage.py runserver {{HOST}}:{{PORT}}
|
||||
|
||||
# Create migrations for all apps
|
||||
makemigrations:
|
||||
uv run python manage.py makemigrations
|
||||
|
||||
# Create migrations for specific app
|
||||
makemigrations-app APP:
|
||||
uv run python manage.py makemigrations {{APP}}
|
||||
|
||||
# Run all migrations
|
||||
migrate:
|
||||
uv run python manage.py migrate
|
||||
|
||||
# Reset database (delete all migrations)
|
||||
reset-db:
|
||||
uv run python manage.py flush --noinput
|
||||
uv run python manage.py migrate
|
||||
|
||||
# Create superuser
|
||||
createsuperuser:
|
||||
uv run python manage.py createsuperuser
|
||||
|
||||
# Collect static files
|
||||
collectstatic:
|
||||
uv run python manage.py collectstatic --noinput
|
||||
|
||||
# Run Django shell
|
||||
shell:
|
||||
uv run python manage.py shell
|
||||
|
||||
# Run Django shell plus (IPython)
|
||||
shell-plus:
|
||||
uv run python manage.py shell_plus
|
||||
|
||||
# Run tests
|
||||
test:
|
||||
uv run python manage.py test
|
||||
|
||||
# Run tests with coverage
|
||||
test-coverage:
|
||||
uv run pytest --cov=. --cov-report=html --cov-report=term
|
||||
|
||||
# Lint code with ruff
|
||||
lint:
|
||||
uv run ruff check .
|
||||
|
||||
# Format code with ruff
|
||||
format:
|
||||
uv run ruff format .
|
||||
|
||||
# Lint and format code
|
||||
style:
|
||||
uv run ruff check .
|
||||
uv run ruff format .
|
||||
|
||||
# Check security issues
|
||||
check:
|
||||
uv run python manage.py check
|
||||
|
||||
# Check security issues (production settings)
|
||||
check-deploy:
|
||||
uv run python manage.py check --deploy
|
||||
|
||||
# Show all database migrations status
|
||||
showmigrations:
|
||||
uv run python manage.py showmigrations
|
||||
|
||||
# Sync dependencies (uv equivalent of pip freeze)
|
||||
requirements:
|
||||
uv pip compile
|
||||
|
||||
# Run all linting and checks
|
||||
ci:
|
||||
uv run ruff check .
|
||||
uv run ruff format --check .
|
||||
uv run python manage.py check
|
||||
|
||||
# Start interactive shell
|
||||
shell-ipython:
|
||||
uv run python manage.py shell_plus
|
||||
|
||||
# Clear all cache
|
||||
clear-cache:
|
||||
uv run python manage.py clear_cache
|
||||
|
||||
# Show Django version
|
||||
version:
|
||||
uv run python manage.py version
|
||||
|
||||
# Run database shell
|
||||
dbshell:
|
||||
uv run python manage.py dbshell
|
||||
|
||||
# Start production server with gunicorn
|
||||
server:
|
||||
uv run gunicorn yoo.wsgi:application --bind 0.0.0.0:8000
|
||||
|
||||
# Start production server with gunicorn workers
|
||||
server-prod WORKERS="4":
|
||||
uv run gunicorn yoo.wsgi:application --bind 0.0.0.0:8000 --workers {{WORKERS}}
|
||||
|
||||
# Start production server with gunicorn and reload on file changes
|
||||
server-watch:
|
||||
uv run gunicorn yoo.wsgi:application --bind 0.0.0.0:8000 --reload
|
||||
|
||||
# Clean Python cache files
|
||||
clean:
|
||||
find . -type d -name "__pycache__" -exec rm -rf {} +
|
||||
find . -type f -name "*.pyc" -delete
|
||||
find . -type f -name "*.pyo" -delete
|
||||
find . -type f -name ".coverage" -delete
|
||||
find . -type d -name "*.egg-info" -exec rm -rf {} +
|
||||
rm -rf htmlcov/
|
||||
rm -rf .pytest_cache/
|
||||
|
||||
# Setup project for development (using uv)
|
||||
setup:
|
||||
uv sync
|
||||
uv run python manage.py migrate
|
||||
uv run python manage.py createsuperuser
|
||||
|
||||
# Complete setup including static files
|
||||
setup-complete:
|
||||
uv sync
|
||||
uv run python manage.py migrate
|
||||
uv run python manage.py collectstatic --noinput
|
||||
uv run python manage.py createsuperuser
|
||||
|
||||
# Install a new package
|
||||
install PACKAGE:
|
||||
uv add {{PACKAGE}}
|
||||
|
||||
# Install a dev package
|
||||
install-dev PACKAGE:
|
||||
uv add --dev {{PACKAGE}}
|
||||
|
||||
# Remove a package
|
||||
remove PACKAGE:
|
||||
uv remove {{PACKAGE}}
|
||||
@@ -1,29 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""generated with djinit"""
|
||||
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", os.getenv("DJANGO_SETTINGS_MODULE"))
|
||||
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
load_dotenv()
|
||||
main()
|
||||
@@ -1,41 +0,0 @@
|
||||
[project]
|
||||
name = "yoo"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
license = { text = "MIT" }
|
||||
dependencies = [
|
||||
"django",
|
||||
"python-dotenv",
|
||||
"django-jazzmin",
|
||||
"djangorestframework",
|
||||
"djangorestframework-simplejwt",
|
||||
"drf-spectacular",
|
||||
"django-cors-headers",
|
||||
"whitenoise",
|
||||
"psycopg2-binary",
|
||||
"gunicorn",
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 88
|
||||
target-version = "py38"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
"E", # pycodestyle errors
|
||||
"W", # pycodestyle warnings
|
||||
"F", # pyflakes
|
||||
"I", # isort
|
||||
"B", # flake8-bugbear
|
||||
"C4", # flake8-comprehensions
|
||||
"UP", # pyupgrade
|
||||
]
|
||||
ignore = [
|
||||
"E501", # line too long, handled by formatter
|
||||
"B008", # do not perform function calls in argument defaults
|
||||
]
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
known-first-party = ["yoo"]
|
||||
@@ -1,14 +0,0 @@
|
||||
#--------------------------------------------------#
|
||||
# The following was generated by djinit: #
|
||||
#--------------------------------------------------#
|
||||
|
||||
Django
|
||||
python-dotenv
|
||||
django-jazzmin
|
||||
djangorestframework
|
||||
djangorestframework_simplejwt
|
||||
drf-spectacular
|
||||
django-cors-headers
|
||||
whitenoise
|
||||
psycopg2-binary
|
||||
gunicorn
|
||||
@@ -1 +0,0 @@
|
||||
python-3.13
|
||||
Reference in new issue
Block a user