Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be4d754b34 | ||
|
|
93c7cd1da2 | ||
|
|
28d298679e | ||
|
|
ad5bd8c699 | ||
|
|
08645b9d90 | ||
|
|
3f39e458c7 | ||
|
|
fe9c38e278 | ||
|
|
b7a8e20595 | ||
|
|
a575863ca0 | ||
|
|
3aef01d4b5 | ||
|
|
6c882864b4 | ||
|
|
fc5de30cb7 | ||
|
|
211219247d | ||
|
|
6fefe8d948 | ||
|
|
b59f4b3f6e | ||
|
|
a8ce63d285 | ||
|
|
82d0af049a | ||
|
|
78cfdb1cc0 | ||
|
|
84db52c906 | ||
|
|
30ac34478b | ||
|
|
cdaeede45a | ||
|
|
33e87e7764 | ||
|
|
079dfff8a5 | ||
|
|
e6ce03fa3c | ||
|
|
e34faae3a7 | ||
|
|
21823fcc61 | ||
|
|
eb59e0c487 | ||
|
|
105cd75595 | ||
|
|
71195af155 | ||
|
|
5fa2003ae1 | ||
|
|
a9e066e694 | ||
|
|
5da43ab3a5 | ||
|
|
f45bacf682 | ||
|
|
5a6e51f52f | ||
|
|
022a5a90af | ||
|
|
f7c9a0764f | ||
|
|
462251059c | ||
|
|
4369cdd44f |
No files matched your search
@@ -6,9 +6,6 @@ updates:
|
||||
interval: "weekly"
|
||||
target-branch: "main"
|
||||
rebase-strategy: "auto"
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "github-actions"
|
||||
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
@@ -16,7 +13,4 @@ updates:
|
||||
interval: "weekly"
|
||||
target-branch: "main"
|
||||
rebase-strategy: "auto"
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "python"
|
||||
open-pull-requests-limit: 10
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
python-version: ["3.13"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v6
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
|
||||
@@ -11,10 +11,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
|
||||
+61
-145
@@ -1,130 +1,26 @@
|
||||
#--------------------------------------------------#
|
||||
# The following was generated with gitignore.nvim: #
|
||||
# The following was generated by djinit: #
|
||||
#--------------------------------------------------#
|
||||
# Gitignore for the following technologies: Python
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
# Python
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
__pycache__/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
dist/
|
||||
build/
|
||||
.eggs/
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
# Django
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
*.sqlite3
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/#use-with-ide
|
||||
.pdm.toml
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
# Environment
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
@@ -133,45 +29,65 @@ ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
# IDEs
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.project
|
||||
.pydevproject
|
||||
.settings/
|
||||
.ropeproject/
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
# OS
|
||||
.DS_Store
|
||||
*.DS_Store
|
||||
Thumbs.db
|
||||
desktop.ini
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
# Django specific
|
||||
media/
|
||||
staticfiles/
|
||||
staticfiles/
|
||||
static_root/
|
||||
/static/
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
# Testing
|
||||
.coverage
|
||||
htmlcov/
|
||||
.pytest_cache/
|
||||
.tox/
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
.cache
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
# Node modules (if using frontend tools)
|
||||
node_modules/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
# Secret keys
|
||||
secret.txt
|
||||
secrets.json
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
|
||||
poetry.toml
|
||||
# Backup files
|
||||
*.bak
|
||||
*.tmp
|
||||
*~
|
||||
|
||||
# ruff
|
||||
.ruff_cache/
|
||||
# Database
|
||||
*.db
|
||||
*.sql
|
||||
|
||||
# LSP config files
|
||||
pyrightconfig.json
|
||||
|
||||
djinit.egg-info/
|
||||
djinit.egg-info/*
|
||||
# Cloud
|
||||
.cloud
|
||||
@@ -1,42 +1,50 @@
|
||||
# djinit
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/Django-4.2%20%7C%205.1%20%7C%205.2-0C4B33?logo=django&logoColor=white" alt="Django">
|
||||
<img src="https://img.shields.io/badge/Python-3.13%2B-3776AB?logo=python&logoColor=white" alt="Python">
|
||||
<a href="https://github.com/S4NKALP/djinit/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License"></a>
|
||||
</p>
|
||||
<div align="center">
|
||||
|
||||
> PyPI did not allow the original name, so the package is released as **djinitx**
|
||||
|
||||
<img src="https://img.shields.io/pypi/v/djinitx?color=blue&label=PyPI&logo=pypi&logoColor=white" alt="PyPI">
|
||||
<img src="https://img.shields.io/badge/Django-4.2%20%7C%205.1%20%7C%205.2-0C4B33?logo=django&logoColor=white" alt="Django">
|
||||
<img src="https://img.shields.io/badge/Python-3.13%2B-3776AB?logo=python&logoColor=white" alt="Python">
|
||||
<a href="https://github.com/S4NKALP/djinit/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License"></a>
|
||||
|
||||
</div>
|
||||
|
||||
A fast, interactive CLI to bootstrap a modern, production‑ready Django project in minutes — featuring split settings, DRF and JWT integration, OpenAPI docs, CORS, static file handling via WhiteNoise, Postgres‑friendly configuration, CI/CD templates, deployment helpers, and battle‑tested defaults that deliver a polished developer experience out of the box.
|
||||
|
||||
- ✨ Split settings: `settings/base.py`, `settings/development.py`, `settings/production.py`
|
||||
- 🧱 Optional nested apps layout (e.g., `apps/`)
|
||||
- 🧩 App scaffolding: URLs, serializers, routes
|
||||
- 🔗 Project URLs wired automatically
|
||||
- 🧰 Utility files: `.gitignore`, `README.md`, `.env.sample`, `requirements.txt`, `pyproject.toml`
|
||||
- 🚀 Deploy helpers: `Justfile`, `Procfile`, `runtime.txt`
|
||||
- 🛠️ CI/CD: GitHub Actions and/or GitLab CI
|
||||
- 🎨 Polished UX with `rich`
|
||||
## Features
|
||||
|
||||
---
|
||||
|
||||
## Demo
|
||||
|
||||

|
||||
|
||||
Tip: If the GIF doesn't load, open `docs/demo.gif` or view it on the repository page.
|
||||
- ✨ **Split settings**: `settings/base.py`, `settings/development.py`, `settings/production.py`
|
||||
- 🏗️ **Three structure types**: Standard Django layout, Predefined structure, or Unified structure
|
||||
- 🧱 **Flexible app layout**: Flat or nested apps package (e.g., `apps/`)
|
||||
- 🧩 **Complete app scaffolding**: URLs, serializers, routes, views, models, admin, tests
|
||||
- 🔗 **Auto-wired URLs**: Project URLs automatically include your apps
|
||||
- 🧰 **Essential utility files**: `.gitignore`, `README.md`, `.env.sample`, `requirements.txt`, `pyproject.toml`
|
||||
- 🚀 **Deployment helpers**: `Justfile`, `Procfile`, `runtime.txt`
|
||||
- 🛠️ **CI/CD templates**: GitHub Actions and/or GitLab CI workflows
|
||||
- 🔐 **Secret key generator**: Generate secure Django secret keys
|
||||
- 🎨 **Polished UX**: Beautiful interactive interface with `rich` library
|
||||
- 📦 **App management**: Add apps to existing projects with automatic settings configuration
|
||||
|
||||
## Installation
|
||||
|
||||
Using pipx (recommended):
|
||||
|
||||
```bash
|
||||
pipx install git+https://github.com/S4NKALP/djinit
|
||||
pipx install djinitx
|
||||
```
|
||||
|
||||
Using pip (user space):
|
||||
Using pip:
|
||||
|
||||
```bash
|
||||
pip install --user git+https://github.com/S4NKALP/djinit
|
||||
pip install djinitx
|
||||
```
|
||||
|
||||
Using uv:
|
||||
|
||||
```bash
|
||||
uv tool install djinitx
|
||||
```
|
||||
|
||||
From source:
|
||||
@@ -47,9 +55,9 @@ cd djinit
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
Requirements: Python 3.13+
|
||||
**Requirements**: Python 3.13+
|
||||
|
||||
## Quick start
|
||||
## Quick Start
|
||||
|
||||
Run the interactive setup:
|
||||
|
||||
@@ -59,83 +67,337 @@ djinit setup
|
||||
dj setup
|
||||
```
|
||||
|
||||
Prompts include:
|
||||
The interactive setup will guide you through:
|
||||
|
||||
- Project directory
|
||||
- Django project name (used by `django-admin startproject`)
|
||||
- Apps layout (flat vs nested package like `apps/`)
|
||||
- App names (comma‑separated)
|
||||
- CI/CD choice (GitHub, GitLab, both, none)
|
||||
- Database config preference (`DATABASE_URL` vs individual params)
|
||||
1. **Structure Type Selection**:
|
||||
- Standard structure (default Django layout)
|
||||
- Predefined structure (`apps/users`, `apps/core`, `api/` layout)
|
||||
- Unified structure (`core/`, `apps/core`, `apps/api` layout)
|
||||
|
||||
2. **Project Configuration**:
|
||||
- Project directory (or use current directory with `.`)
|
||||
- Django project name (used for the config module)
|
||||
- Apps layout (flat vs nested package like `apps/`)
|
||||
- App names (comma‑separated)
|
||||
|
||||
3. **CI/CD Configuration**:
|
||||
- GitHub Actions only
|
||||
- GitLab CI only
|
||||
- Both (GitHub Actions + GitLab CI)
|
||||
- None (skip CI/CD)
|
||||
|
||||
4. **Database Configuration**:
|
||||
- Use `DATABASE_URL` (recommended for production)
|
||||
- Use individual database parameters
|
||||
|
||||
## Commands
|
||||
|
||||
- `djinit setup` or `dj setup` — Launch the interactive project generator
|
||||
- `djinit app <names>` or `dj app <names>` — Create one or more Django apps in an existing project
|
||||
- Examples:
|
||||
- `djinit app users` or `dj app users`
|
||||
- `djinit app users,products,orders` or `dj app users,products,orders`
|
||||
- `djinit app users products orders` or `dj app users products orders`
|
||||
- `djinit secret [--count N] [--length L]` or `dj secret [--count N] [--length L]` — Print random Django `SECRET_KEY` values
|
||||
- Example: `djinit secret --count 5 --length 50`
|
||||
### Setup Command
|
||||
|
||||
## What gets generated
|
||||
Launch the interactive project generator:
|
||||
|
||||
- Django project and apps, then structure validation
|
||||
- Settings package and split settings
|
||||
- Base/development/production settings
|
||||
- Utility files
|
||||
- App files: `urls.py`, `serializers.py`, `routes.py`
|
||||
- Project `urls.py` (auto‑includes your apps)
|
||||
- Updated `wsgi.py`, `asgi.py`, `manage.py`
|
||||
- `Justfile`, `Procfile`, `runtime.txt`
|
||||
- CI/CD workflows (per your selection)
|
||||
```bash
|
||||
djinit setup
|
||||
# or
|
||||
dj setup
|
||||
```
|
||||
|
||||
### Output highlights
|
||||
### App Command
|
||||
|
||||
- `config/settings/` → `__init__.py`, `base.py`, `development.py`, `production.py`
|
||||
- `apps/<app>/` → `urls.py`, `serializers.py`, `routes.py`
|
||||
- Project root → `.gitignore`, `README.md`, `.env.sample`, `requirements.txt`, `pyproject.toml`, `Justfile`, `Procfile`, `runtime.txt`
|
||||
- CI/CD → `.github/workflows/` and/or `.gitlab-ci.yml`
|
||||
Create one or more Django apps in an existing project:
|
||||
|
||||
Note: If you choose a nested apps package (e.g., `apps`), imports and `INSTALLED_APPS` entries are configured accordingly.
|
||||
```bash
|
||||
djinit app <names>
|
||||
# or
|
||||
dj app <names>
|
||||
```
|
||||
|
||||
## Included packages (by default)
|
||||
**Examples**:
|
||||
|
||||
Generated `requirements.txt` includes:
|
||||
- `djinit app users` or `dj app users`
|
||||
- `djinit app users,products,orders` or `dj app users,products,orders`
|
||||
- `djinit app users products orders` or `dj app users products orders`
|
||||
|
||||
- Django
|
||||
- python-dotenv
|
||||
- django-jazzmin
|
||||
- djangorestframework
|
||||
- djangorestframework-simplejwt
|
||||
- drf-spectacular
|
||||
- django-cors-headers
|
||||
- whitenoise
|
||||
- psycopg2-binary
|
||||
- gunicorn
|
||||
- dj-database-url (when you opt into `DATABASE_URL`)
|
||||
The app command automatically:
|
||||
|
||||
These give you a ready‑to‑use stack with DRF, JWT auth, OpenAPI schema generation, CORS, static files in production, and Postgres support.
|
||||
- Creates the app with all necessary files
|
||||
- Adds the app to `INSTALLED_APPS` in `settings/base.py`
|
||||
- Configures URLs if using predefined structure
|
||||
- Detects and respects your project's structure (nested/flat)
|
||||
|
||||
## Environment and database
|
||||
### Secret Command
|
||||
|
||||
- A `.env.sample` is generated to get you started.
|
||||
- If you opt into `DATABASE_URL`, `production.py` expects an environment variable like:
|
||||
`postgres://user:password@host:port/database`
|
||||
Generate secure Django `SECRET_KEY` values:
|
||||
|
||||
```bash
|
||||
djinit secret [--count N] [--length L]
|
||||
# or
|
||||
dj secret [--count N] [--length L]
|
||||
```
|
||||
|
||||
**Examples**:
|
||||
|
||||
- `djinit secret` - Generate 3 keys with default length (50)
|
||||
- `djinit secret --count 5 --length 50` - Generate 5 keys of length 50
|
||||
- `dj secret --count 10 --length 64` - Generate 10 keys of length 64
|
||||
|
||||
## Project Structure Types
|
||||
|
||||
### Standard Structure
|
||||
|
||||
The default Django layout with split settings:
|
||||
|
||||
```
|
||||
project_name/
|
||||
├── manage.py
|
||||
├── project_name/
|
||||
│ ├── __init__.py
|
||||
│ ├── settings/
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── base.py
|
||||
│ │ ├── development.py
|
||||
│ │ └── production.py
|
||||
│ ├── urls.py
|
||||
│ ├── wsgi.py
|
||||
│ └── asgi.py
|
||||
├── apps/ # Optional nested apps
|
||||
│ └── <app_name>/
|
||||
│ ├── urls.py
|
||||
│ ├── serializers.py
|
||||
│ ├── routes.py
|
||||
│ └── ...
|
||||
└── <app_name>/ # Or flat apps
|
||||
└── ...
|
||||
```
|
||||
|
||||
### Predefined Structure
|
||||
|
||||
A production-ready structure with `apps/` and `api/` packages:
|
||||
|
||||
```
|
||||
project_name/
|
||||
├── manage.py
|
||||
├── config/ # Django config module
|
||||
│ ├── settings/
|
||||
│ │ ├── base.py
|
||||
│ │ ├── development.py
|
||||
│ │ └── production.py
|
||||
│ └── urls.py
|
||||
├── apps/
|
||||
│ ├── users/ # Pre-configured users app
|
||||
│ │ ├── models/
|
||||
│ │ ├── serializers/
|
||||
│ │ ├── views/
|
||||
│ │ ├── services/
|
||||
│ │ └── tests/
|
||||
│ └── core/ # Core utilities
|
||||
│ ├── exceptions.py
|
||||
│ ├── utils/
|
||||
│ ├── mixins/
|
||||
│ └── middleware/
|
||||
└── api/
|
||||
├── urls.py
|
||||
└── v1/
|
||||
└── urls.py
|
||||
```
|
||||
|
||||
### Unified Structure
|
||||
|
||||
A unified structure with `core/` as the main module:
|
||||
|
||||
```
|
||||
project_name/
|
||||
├── manage.py
|
||||
├── core/ # Main Django config
|
||||
│ ├── settings/
|
||||
│ │ ├── base.py
|
||||
│ │ ├── development.py
|
||||
│ │ └── production.py
|
||||
│ ├── urls.py
|
||||
│ ├── wsgi.py
|
||||
│ └── asgi.py
|
||||
└── apps/
|
||||
├── core/ # Core app with models, utils
|
||||
│ ├── models/
|
||||
│ └── utils/
|
||||
└── api/ # API app
|
||||
└── ...
|
||||
```
|
||||
|
||||
## What Gets Generated
|
||||
|
||||
### Core Files
|
||||
|
||||
- **Django project structure** with split settings
|
||||
- **Settings package**: `base.py`, `development.py`, `production.py`
|
||||
- **Project URLs**: Auto-configured with app includes
|
||||
- **WSGI/ASGI**: Production-ready application entry points
|
||||
- **manage.py**: Django management script
|
||||
|
||||
### App Files
|
||||
|
||||
Each app includes:
|
||||
|
||||
- `urls.py` - URL routing
|
||||
- `serializers.py` - DRF serializers
|
||||
- `routes.py` - API route definitions
|
||||
- `views.py` - View classes
|
||||
- `models.py` - Database models
|
||||
- `admin.py` - Admin configuration
|
||||
- `tests.py` - Test structure
|
||||
- `apps.py` - App configuration
|
||||
- `migrations/` - Migration directory
|
||||
|
||||
### Utility Files
|
||||
|
||||
- `.gitignore` - Comprehensive Python/Django gitignore
|
||||
- `README.md` - Project documentation template
|
||||
- `.env.sample` - Environment variables template (includes `SECRET_KEY` placeholder, `DATABASE_URL` or individual DB params, email settings)
|
||||
- `requirements.txt` - All necessary dependencies
|
||||
- `pyproject.toml` - Modern Python project configuration (includes ruff linting/formatting config)
|
||||
- `Justfile` - Development commands (migrations, server, etc.) using `uv run`
|
||||
- `Procfile` - PaaS deployment configuration (Heroku, Railway, Render, etc.) with release task for migrations
|
||||
- `runtime.txt` - Python version specification
|
||||
|
||||
### CI/CD Files
|
||||
|
||||
- `.github/workflows/ci.yml` - GitHub Actions workflow (if selected)
|
||||
- `.gitlab-ci.yml` - GitLab CI configuration (if selected)
|
||||
|
||||
## Included Packages
|
||||
|
||||
The generated `requirements.txt` includes:
|
||||
|
||||
- **Django** - Web framework
|
||||
- **python-dotenv** - Environment variable management
|
||||
- **django-jazzmin** - Modern Django admin interface
|
||||
- **djangorestframework** - REST API framework
|
||||
- **djangorestframework_simplejwt** - JWT authentication (note: underscore in package name)
|
||||
- **drf-spectacular** - OpenAPI 3.0 schema generation
|
||||
- **django-cors-headers** - CORS handling
|
||||
- **whitenoise** - Static file serving for production
|
||||
- **psycopg2-binary** - PostgreSQL adapter
|
||||
- **gunicorn** - Production WSGI server
|
||||
- **dj-database-url** - Database URL parsing (when using `DATABASE_URL`)
|
||||
|
||||
This gives you a complete stack with:
|
||||
|
||||
- REST API with DRF
|
||||
- JWT authentication endpoints: `/token/`, `/token/refresh/`, `/token/blacklist/`
|
||||
- OpenAPI documentation at `/docs/` and `/schema/` (available in DEBUG mode only)
|
||||
- CORS support (configured for localhost:3000 in development)
|
||||
- Production-ready static file handling with WhiteNoise
|
||||
- PostgreSQL support (SQLite in development by default)
|
||||
|
||||
## Environment and Database
|
||||
|
||||
### Environment Variables
|
||||
|
||||
A `.env.sample` file is generated with:
|
||||
|
||||
- `DJANGO_SETTINGS_MODULE` - Settings module path
|
||||
- `SECRET_KEY` - Placeholder for your secret key (use `djinit secret` to generate)
|
||||
- `ALLOWED_HOSTS` - Comma-separated list of allowed hosts
|
||||
- Database configuration (either `DATABASE_URL` or individual `DB_NAME`, `DB_USER`, `DB_PASSWORD`, `DB_HOST`, `DB_PORT`)
|
||||
- Email settings (SMTP configuration)
|
||||
|
||||
### Database Configuration
|
||||
|
||||
**Development (Default)**
|
||||
|
||||
- Uses SQLite (`db.sqlite3`) for local development
|
||||
- No database configuration needed
|
||||
|
||||
**Production**
|
||||
|
||||
**Option 1: DATABASE_URL (Recommended)**
|
||||
|
||||
If you opt into `DATABASE_URL`, the production settings use `dj-database-url`:
|
||||
|
||||
```
|
||||
DATABASE_URL=postgres://user:password@host:port/database
|
||||
```
|
||||
|
||||
**Option 2: Individual Parameters**
|
||||
|
||||
Traditional Django database configuration with separate environment variables:
|
||||
|
||||
- `DB_NAME` - Database name
|
||||
- `DB_USER` - Database user
|
||||
- `DB_PASSWORD` - Database password
|
||||
- `DB_HOST` - Database host
|
||||
- `DB_PORT` - Database port
|
||||
|
||||
## Development Workflow
|
||||
|
||||
After setup, use the generated `Justfile` for common tasks (uses `uv` by default):
|
||||
|
||||
```bash
|
||||
just dev # Start development server (uv run python manage.py runserver)
|
||||
just migrate # Run migrations
|
||||
just makemigrations # Create migrations
|
||||
just shell # Django shell
|
||||
just test # Run tests
|
||||
just format # Format code with ruff
|
||||
just lint # Lint code with ruff
|
||||
just setup # Complete setup (uv sync + migrate + createsuperuser)
|
||||
just server # Start production server with gunicorn
|
||||
```
|
||||
|
||||
**Note**: The Justfile uses `uv run` for all commands. If you're not using `uv`, you can modify the Justfile or use Django commands directly.
|
||||
|
||||
### API Endpoints
|
||||
|
||||
The generated project includes:
|
||||
|
||||
- **Admin**: `/admin/`
|
||||
- **JWT Authentication**:
|
||||
- `/token/` - Obtain access token
|
||||
- `/token/refresh/` - Refresh access token
|
||||
- `/token/blacklist/` - Blacklist refresh token
|
||||
- **API Documentation** (DEBUG mode only):
|
||||
- `/docs/` - Swagger UI
|
||||
- `/schema/` - OpenAPI schema
|
||||
|
||||
### Settings Configuration
|
||||
|
||||
- **Base Settings** (`settings/base.py`):
|
||||
- DRF configuration with JWT authentication
|
||||
- CORS settings
|
||||
- WhiteNoise for static files
|
||||
- Security headers
|
||||
- Pagination (20 items per page)
|
||||
|
||||
- **Development Settings** (`settings/development.py`):
|
||||
- SQLite database
|
||||
- DEBUG = True
|
||||
- Console email backend
|
||||
- CORS allows all origins
|
||||
- Generated secret key (replace in production)
|
||||
|
||||
- **Production Settings** (`settings/production.py`):
|
||||
- PostgreSQL database (via `DATABASE_URL` or individual params)
|
||||
- DEBUG = False
|
||||
- Security settings (HTTPS redirect, HSTS, secure cookies)
|
||||
- SMTP email configuration
|
||||
- Secret key from environment variable
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome!
|
||||
Please open an issue for bugs/ideas. PRs with clear descriptions are appreciated.
|
||||
Contributions are welcome! Please:
|
||||
|
||||
1. Open an issue for bugs or feature ideas
|
||||
2. Fork the repository
|
||||
3. Create a feature branch
|
||||
4. Submit a pull request with a clear description
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
- Django and the Django community
|
||||
- Jinja2
|
||||
- rich
|
||||
- click
|
||||
- ruff
|
||||
- Jinja2 - Template engine
|
||||
- rich - Beautiful terminal output
|
||||
- click - CLI framework
|
||||
- ruff - Fast Python linter and formatter
|
||||
|
||||
## License
|
||||
|
||||
|
||||
+57
-31
@@ -6,13 +6,10 @@ Coordinates between different managers to create a complete Django project.
|
||||
import os
|
||||
from typing import Callable, List, Tuple
|
||||
|
||||
from djinit.scripts.cicd_config_generator import CICDConfigGenerator
|
||||
from djinit.scripts.console_ui import UIFormatter
|
||||
from djinit.scripts.deployment_files_generator import DeploymentConfigGenerator
|
||||
from djinit.scripts.files import FileManager
|
||||
from djinit.scripts.project import ProjectManager
|
||||
from djinit.scripts.secretkey_generator import generate_secret_command
|
||||
from djinit.scripts.settings import SettingsManager
|
||||
|
||||
|
||||
class Cli:
|
||||
@@ -22,37 +19,66 @@ class Cli:
|
||||
self.primary_app = primary_app
|
||||
self.app_names = app_names
|
||||
self.metadata = metadata
|
||||
self.project_root = os.path.join(os.getcwd(), project_dir)
|
||||
if project_dir == ".": # handle '.' for current directory
|
||||
self.project_root = os.getcwd()
|
||||
else:
|
||||
self.project_root = os.path.join(os.getcwd(), project_dir)
|
||||
|
||||
# Initialize managers
|
||||
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)
|
||||
|
||||
def run_setup(self) -> bool:
|
||||
steps: List[Tuple[str, Callable[[], bool]]] = [
|
||||
("Creating Django project", self.project_manager.create_project),
|
||||
("Creating Django apps", self.project_manager.create_apps),
|
||||
("Validating project structure", self.project_manager.validate_project_structure),
|
||||
("Setting up project structure", self.settings_manager.create_settings_structure),
|
||||
("Configuring base settings", self.settings_manager.update_base_settings),
|
||||
("Configuring development settings", self.settings_manager.update_development_settings),
|
||||
("Configuring production settings", self.settings_manager.update_production_settings),
|
||||
("Creating utility files", self._create_utility_files),
|
||||
("Setting up app URLs", self.file_manager.create_app_urls),
|
||||
("Creating app serializers", self.file_manager.create_app_serializers),
|
||||
("Creating app routes", self.file_manager.create_app_routes),
|
||||
("Configuring comprehensive URLs", self.file_manager.update_project_urls),
|
||||
("Updating WSGI configuration", self.file_manager.update_wsgi_file),
|
||||
("Updating ASGI configuration", self.file_manager.update_asgi_file),
|
||||
("Updating manage.py", self.file_manager.update_manage_py),
|
||||
("Creating Justfile", self.deployment_manager.create_justfile),
|
||||
("Creating Procfile", self.deployment_manager.create_procfile),
|
||||
("Creating runtime.txt", lambda: self.deployment_manager.create_runtime_txt("3.13")),
|
||||
("Creating CI/CD pipelines", self._create_cicd_pipelines),
|
||||
]
|
||||
# If predefined structure is enabled, adjust metadata defaults
|
||||
if self.metadata.get("predefined_structure"):
|
||||
# Default module name to 'config' for conventional layout
|
||||
self.metadata.setdefault("project_module_name", "config")
|
||||
# Ensure nested apps live under 'apps'
|
||||
self.metadata.setdefault("nested_apps", True)
|
||||
self.metadata.setdefault("nested_dir", "apps")
|
||||
# Default apps for predefined structure
|
||||
if not self.app_names:
|
||||
self.app_names = ["users", "core"]
|
||||
# propagate to managers
|
||||
self.project_manager.app_names = self.app_names
|
||||
|
||||
# If unified structure is enabled, adjust metadata defaults
|
||||
if self.metadata.get("unified_structure"):
|
||||
# For unified structure, module name is 'core'
|
||||
self.metadata.setdefault("project_module_name", "core")
|
||||
# Ensure nested apps live under 'apps'
|
||||
self.metadata.setdefault("nested_apps", True)
|
||||
self.metadata.setdefault("nested_dir", "apps")
|
||||
# Default apps for unified structure (empty, apps/core and apps/api are created by default)
|
||||
if not self.app_names:
|
||||
self.app_names = []
|
||||
# propagate to managers
|
||||
self.project_manager.app_names = self.app_names
|
||||
|
||||
steps: List[Tuple[str, Callable[[], bool]]] = []
|
||||
|
||||
steps.append(("Creating Django project", self.project_manager.create_project))
|
||||
|
||||
if self.metadata.get("unified_structure"):
|
||||
# Build unified structure
|
||||
steps.append(("Creating unified structure", self.file_manager.create_unified_structure))
|
||||
elif self.metadata.get("predefined_structure"):
|
||||
# Build custom tree and then inject apps into settings
|
||||
steps.append(("Creating predefined structure", self.file_manager.create_predefined_structure))
|
||||
steps.append(("Adding apps to settings", self.project_manager.add_apps_to_settings))
|
||||
else:
|
||||
steps.append(("Creating Django apps", self.project_manager.create_apps))
|
||||
steps.append(("Creating project URLs", self.file_manager.create_project_urls))
|
||||
|
||||
steps.extend(
|
||||
[
|
||||
("Validating project structure", self.project_manager.validate_project_structure),
|
||||
("Creating utility files", self._create_utility_files),
|
||||
("Creating Procfile", self.file_manager.create_procfile),
|
||||
("Creating Justfile", self.file_manager.create_justfile),
|
||||
("Creating runtime.txt", self.file_manager.create_runtime_txt),
|
||||
("Creating CI/CD pipelines", self._create_cicd_pipelines),
|
||||
]
|
||||
)
|
||||
|
||||
total_steps = len(steps)
|
||||
success = True
|
||||
@@ -93,12 +119,12 @@ class Cli:
|
||||
|
||||
def _create_cicd_pipelines(self) -> bool:
|
||||
if self.metadata.get("use_github_actions", True):
|
||||
result = self.cicd_manager.create_github_actions()
|
||||
result = self.file_manager.create_github_actions()
|
||||
if not result:
|
||||
return False
|
||||
|
||||
if self.metadata.get("use_gitlab_ci", True):
|
||||
result = self.cicd_manager.create_gitlab_ci()
|
||||
result = self.file_manager.create_gitlab_ci()
|
||||
if not result:
|
||||
return False
|
||||
|
||||
|
||||
+1
-9
@@ -25,7 +25,6 @@ def clear_screen() -> None:
|
||||
|
||||
|
||||
def main() -> None:
|
||||
# Parse command line arguments
|
||||
args = parse_arguments()
|
||||
|
||||
# Handle secret command
|
||||
@@ -39,7 +38,6 @@ def main() -> None:
|
||||
return
|
||||
|
||||
# Default to setup command
|
||||
# Clear screen
|
||||
clear_screen()
|
||||
|
||||
# Display welcome screen
|
||||
@@ -47,15 +45,12 @@ def main() -> None:
|
||||
console.print(UIFormatter.create_welcome_panel())
|
||||
console.print()
|
||||
|
||||
# Get user input
|
||||
project_dir, project_name, primary_app, app_names, metadata = get_user_input()
|
||||
|
||||
# Confirm setup
|
||||
if not confirm_setup(project_dir, project_name, app_names, metadata):
|
||||
UIFormatter.print_info("Setup cancelled by user.")
|
||||
return
|
||||
|
||||
# Run setup
|
||||
console.print()
|
||||
UIFormatter.print_info("Starting Django project setup...")
|
||||
console.print()
|
||||
@@ -63,10 +58,7 @@ def main() -> None:
|
||||
django_cli = Cli(project_dir, project_name, primary_app, app_names, metadata)
|
||||
success = django_cli.run_setup()
|
||||
|
||||
# Display completion summary
|
||||
UIFormatter.create_summary_panel(project_dir, project_name, app_names, True)
|
||||
|
||||
# Exit with appropriate code
|
||||
UIFormatter.create_summary_panel(project_dir, project_name, app_names, True, metadata=metadata)
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
|
||||
|
||||
+146
-167
@@ -4,12 +4,22 @@ Handles creation of Django apps and updating settings.
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
from typing import Optional
|
||||
|
||||
from djinit.scripts.console_ui import UIFormatter
|
||||
from djinit.scripts.template_engine import template_engine
|
||||
from djinit.utils import change_cwd, create_file_with_content
|
||||
from djinit.scripts.django_helper import DjangoHelper
|
||||
from djinit.utils import (
|
||||
calculate_app_module_path,
|
||||
create_directory_with_init,
|
||||
create_file_from_template,
|
||||
create_init_file,
|
||||
detect_nested_structure_from_settings,
|
||||
extract_existing_apps,
|
||||
find_project_dir,
|
||||
find_settings_path,
|
||||
insert_apps_into_user_defined_apps,
|
||||
is_django_project,
|
||||
)
|
||||
|
||||
|
||||
class AppManager:
|
||||
@@ -17,6 +27,7 @@ class AppManager:
|
||||
self.app_name = app_name
|
||||
self.current_dir = os.getcwd()
|
||||
self.manage_py_path = os.path.join(self.current_dir, "manage.py")
|
||||
self._project_structure_cache = None
|
||||
|
||||
def create_app(self) -> bool:
|
||||
if not self._is_django_project():
|
||||
@@ -35,40 +46,42 @@ class AppManager:
|
||||
if not self._add_to_installed_apps():
|
||||
return False
|
||||
|
||||
if not self._create_additional_files():
|
||||
return False
|
||||
|
||||
UIFormatter.print_success(f"Django app '{self.app_name}' created and configured successfully!")
|
||||
return True
|
||||
|
||||
def _is_django_project(self) -> bool:
|
||||
return os.path.exists(self.manage_py_path)
|
||||
return is_django_project(self.current_dir)
|
||||
|
||||
def _app_exists(self) -> bool:
|
||||
# Detect project structure to check in correct location
|
||||
is_nested, nested_dir, apps_base_dir = self._detect_project_structure()
|
||||
app_path = os.path.join(apps_base_dir, self.app_name)
|
||||
# For predefined structure, check in apps/ directory
|
||||
if self._is_predefined_structure():
|
||||
app_path = os.path.join(self.current_dir, "apps", self.app_name)
|
||||
else:
|
||||
_, _, apps_base_dir = self._get_project_structure()
|
||||
app_path = os.path.join(apps_base_dir, self.app_name)
|
||||
return os.path.exists(app_path)
|
||||
|
||||
def _create_django_app(self) -> bool:
|
||||
# Detect project structure
|
||||
is_nested, nested_dir, apps_base_dir = self._detect_project_structure()
|
||||
|
||||
# manage.py is always in the project root (current_dir), not in the project directory
|
||||
manage_py_path = os.path.join(self.current_dir, "manage.py")
|
||||
if not os.path.exists(manage_py_path):
|
||||
# Verify manage.py exists in project root
|
||||
if not os.path.exists(self.manage_py_path):
|
||||
UIFormatter.print_error("Could not find manage.py file in project root")
|
||||
return False
|
||||
|
||||
# Create app in the correct location
|
||||
with change_cwd(apps_base_dir):
|
||||
subprocess.run(["django-admin", "startapp", self.app_name], capture_output=True, text=True, check=True)
|
||||
# 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"))
|
||||
|
||||
UIFormatter.print_success(f"Created Django app '{self.app_name}' in {apps_base_dir}")
|
||||
return True
|
||||
# Otherwise, use standard flat app generation
|
||||
_, _, apps_base_dir = self._get_project_structure()
|
||||
success = DjangoHelper.startapp(self.app_name, apps_base_dir)
|
||||
if success:
|
||||
UIFormatter.print_success(f"Created Django app '{self.app_name}' in {apps_base_dir}")
|
||||
else:
|
||||
UIFormatter.print_error(f"Failed to create Django app '{self.app_name}'")
|
||||
return success
|
||||
|
||||
def _add_to_installed_apps(self) -> bool:
|
||||
settings_path = self._find_settings_path()
|
||||
settings_path = find_settings_path(self.current_dir)
|
||||
if not settings_path:
|
||||
UIFormatter.print_error("Could not find Django settings directory")
|
||||
return False
|
||||
@@ -81,30 +94,17 @@ class AppManager:
|
||||
with open(base_settings_path) as f:
|
||||
content = f.read()
|
||||
|
||||
# Detect project structure to determine the correct app module path
|
||||
is_nested, nested_dir, apps_base_dir = self._detect_project_structure()
|
||||
|
||||
# Determine the correct module path for the app
|
||||
if is_nested and nested_dir:
|
||||
app_module_path = f"{nested_dir}.{self.app_name}"
|
||||
# Get the app's module path (e.g., "apps.users" or just "users")
|
||||
# For predefined structure, use "apps" as nested_dir
|
||||
if self._is_predefined_structure():
|
||||
app_module_path = f"apps.{self.app_name}"
|
||||
else:
|
||||
app_module_path = self.app_name
|
||||
is_nested, nested_dir, _ = self._get_project_structure()
|
||||
app_module_path = calculate_app_module_path(self.app_name, is_nested, nested_dir)
|
||||
|
||||
# Check if app is already in USER_DEFINED_APPS section specifically
|
||||
lines = content.split("\n")
|
||||
in_user_apps = False
|
||||
app_already_exists = False
|
||||
|
||||
for line in lines:
|
||||
if "USER_DEFINED_APPS" in line and "=" in line:
|
||||
in_user_apps = True
|
||||
elif in_user_apps and line.strip() == "]":
|
||||
in_user_apps = False
|
||||
elif in_user_apps and (f'"{app_module_path}"' in line or f"'{app_module_path}'" in line):
|
||||
app_already_exists = True
|
||||
break
|
||||
|
||||
if app_already_exists:
|
||||
# Check if app is already in USER_DEFINED_APPS
|
||||
existing_apps = extract_existing_apps(content)
|
||||
if app_module_path in existing_apps:
|
||||
UIFormatter.print_success(f"App '{app_module_path}' already configured in USER_DEFINED_APPS")
|
||||
return True
|
||||
|
||||
@@ -112,143 +112,122 @@ class AppManager:
|
||||
UIFormatter.print_error("Could not find USER_DEFINED_APPS section in base.py")
|
||||
return False
|
||||
|
||||
lines = content.split("\n")
|
||||
new_lines = []
|
||||
in_user_apps = False
|
||||
apps_added = False
|
||||
|
||||
for line in lines:
|
||||
if "USER_DEFINED_APPS" in line and "=" in line:
|
||||
in_user_apps = True
|
||||
new_lines.append(line)
|
||||
elif in_user_apps and line.strip() == "]":
|
||||
new_lines.append(f' "{app_module_path}",')
|
||||
new_lines.append(line)
|
||||
in_user_apps = False
|
||||
apps_added = True
|
||||
else:
|
||||
new_lines.append(line)
|
||||
|
||||
if not apps_added:
|
||||
UIFormatter.print_error("Could not find USER_DEFINED_APPS section in base.py")
|
||||
# Add app to USER_DEFINED_APPS list in settings file
|
||||
updated_content = insert_apps_into_user_defined_apps(content, [app_module_path])
|
||||
if not updated_content:
|
||||
return False
|
||||
|
||||
with open(base_settings_path, "w") as f:
|
||||
f.write("\n".join(new_lines))
|
||||
f.write(updated_content)
|
||||
|
||||
UIFormatter.print_success(f"Added '{app_module_path}' to USER_DEFINED_APPS in base.py")
|
||||
return True
|
||||
|
||||
def _find_settings_path(self) -> Optional[str]:
|
||||
# Look for project directories that contain settings
|
||||
for item in os.listdir(self.current_dir):
|
||||
if os.path.isdir(item) and not item.startswith(".") and item != "__pycache__":
|
||||
# Check if this directory contains Django project files
|
||||
project_path = os.path.join(self.current_dir, item)
|
||||
settings_path = os.path.join(project_path, "settings")
|
||||
def _get_project_structure(self) -> tuple[bool, Optional[str], str]:
|
||||
"""Get project structure with caching to avoid repeated detection."""
|
||||
if self._project_structure_cache is None:
|
||||
# Find project directory with settings/base.py
|
||||
project_dir, settings_base_path = find_project_dir(self.current_dir)
|
||||
|
||||
# Check if settings directory exists with base.py
|
||||
if os.path.exists(settings_path) and os.path.exists(os.path.join(settings_path, "base.py")):
|
||||
return settings_path
|
||||
if project_dir is None:
|
||||
self._project_structure_cache = (False, None, self.current_dir)
|
||||
else:
|
||||
# Detect nested structure from settings file
|
||||
self._project_structure_cache = detect_nested_structure_from_settings(
|
||||
settings_base_path, self.current_dir
|
||||
)
|
||||
return self._project_structure_cache
|
||||
|
||||
# Fallback: look for settings in current directory
|
||||
settings_path = os.path.join(self.current_dir, "settings")
|
||||
if os.path.exists(settings_path) and os.path.exists(os.path.join(settings_path, "base.py")):
|
||||
return settings_path
|
||||
def _is_predefined_structure(self) -> bool:
|
||||
# Heuristic: presence of 'apps' directory at project root (where manage.py lives) and 'api' dir
|
||||
apps_dir = os.path.join(self.current_dir, "apps")
|
||||
api_dir = os.path.join(self.current_dir, "api")
|
||||
return os.path.isdir(apps_dir) and os.path.isdir(api_dir)
|
||||
|
||||
return None
|
||||
def _create_predefined_app(self, apps_dir: str) -> bool:
|
||||
"""Create an app following the predefined nested structure with custom templates."""
|
||||
app_dir = os.path.join(apps_dir, self.app_name)
|
||||
os.makedirs(app_dir, exist_ok=True)
|
||||
create_init_file(app_dir, f"Created apps/{self.app_name}/__init__.py")
|
||||
|
||||
def _detect_project_structure(self) -> tuple[bool, Optional[str], str]:
|
||||
# Step 1: find project directory containing settings/base.py
|
||||
project_dir: Optional[str] = None
|
||||
settings_base_path: Optional[str] = None
|
||||
for item in os.listdir(self.current_dir):
|
||||
if os.path.isdir(item) and not item.startswith(".") and item != "__pycache__":
|
||||
candidate = os.path.join(self.current_dir, item)
|
||||
base_py = os.path.join(candidate, "settings", "base.py")
|
||||
if os.path.exists(base_py):
|
||||
project_dir = candidate
|
||||
settings_base_path = base_py
|
||||
break
|
||||
# Compute names for generic templates
|
||||
model_class_name = "".join([part.capitalize() for part in self.app_name.split("_")])
|
||||
app_module = f"apps.{self.app_name}"
|
||||
base_context = {"app_name": self.app_name, "app_module": app_module, "model_class_name": model_class_name}
|
||||
|
||||
# Fallback: current_dir/settings/base.py
|
||||
if project_dir is None:
|
||||
base_py = os.path.join(self.current_dir, "settings", "base.py")
|
||||
if os.path.exists(base_py):
|
||||
project_dir = self.current_dir
|
||||
settings_base_path = base_py
|
||||
|
||||
if project_dir is None:
|
||||
# Not a Django project structure
|
||||
return False, None, self.current_dir
|
||||
|
||||
# Step 2: try to infer nested_dir from USER_DEFINED_APPS in base.py
|
||||
try:
|
||||
if settings_base_path and os.path.exists(settings_base_path):
|
||||
with open(settings_base_path) as f:
|
||||
base_content = f.read()
|
||||
# Find lines inside USER_DEFINED_APPS and extract first dotted app
|
||||
in_user_apps = False
|
||||
for line in base_content.splitlines():
|
||||
if "USER_DEFINED_APPS" in line and "=" in line:
|
||||
in_user_apps = True
|
||||
continue
|
||||
if in_user_apps and "]" in line:
|
||||
break
|
||||
if in_user_apps:
|
||||
line_stripped = line.strip().strip(",")
|
||||
if line_stripped.startswith('"') or line_stripped.startswith("'"):
|
||||
app_label = line_stripped.strip("\"'")
|
||||
if "." in app_label:
|
||||
prefix = app_label.split(".", 1)[0]
|
||||
nested_path = os.path.join(self.current_dir, prefix)
|
||||
if os.path.isdir(nested_path):
|
||||
return True, prefix, nested_path
|
||||
except Exception:
|
||||
# Ignore parsing issues and continue with heuristics
|
||||
pass
|
||||
|
||||
# Step 3: look for existing package directories in project root
|
||||
preferred = ["apps", "modules", "packages"]
|
||||
for name in preferred:
|
||||
nested_path = os.path.join(self.current_dir, name)
|
||||
if os.path.isdir(nested_path) and os.path.exists(os.path.join(nested_path, "__init__.py")):
|
||||
return True, name, nested_path
|
||||
|
||||
# Any other directory with __init__.py qualifies as a package
|
||||
for item in os.listdir(self.current_dir):
|
||||
if item in ("__pycache__",) or item.startswith("."):
|
||||
continue
|
||||
nested_path = os.path.join(self.current_dir, item)
|
||||
if os.path.isdir(nested_path) and os.path.exists(os.path.join(nested_path, "__init__.py")):
|
||||
return True, item, nested_path
|
||||
|
||||
# Step 4: fallback
|
||||
return False, None, self.current_dir
|
||||
|
||||
def _create_additional_files(self) -> bool:
|
||||
"""Create additional files for the app (serializers.py, routes.py)."""
|
||||
# Detect project structure to get correct app path
|
||||
is_nested, nested_dir, apps_base_dir = self._detect_project_structure()
|
||||
app_path = os.path.join(apps_base_dir, self.app_name)
|
||||
|
||||
# Create serializers.py
|
||||
context = {"app_name": self.app_name}
|
||||
serializers_content = template_engine.render_template("serializers.j2", context)
|
||||
create_file_with_content(
|
||||
os.path.join(app_path, "serializers.py"),
|
||||
serializers_content,
|
||||
f"Created {self.app_name}/serializers.py",
|
||||
should_format=True,
|
||||
# Create apps.py
|
||||
create_file_from_template(
|
||||
os.path.join(app_dir, "apps.py"),
|
||||
"base/apps.j2",
|
||||
{"app_name": self.app_name},
|
||||
f"Created apps/{self.app_name}/apps.py",
|
||||
)
|
||||
|
||||
# Create routes.py
|
||||
routes_content = template_engine.render_template("routes.j2", context)
|
||||
create_file_with_content(
|
||||
os.path.join(app_path, "routes.py"),
|
||||
routes_content,
|
||||
f"Created {self.app_name}/routes.py",
|
||||
should_format=True,
|
||||
# Subfolders
|
||||
subfolders = {
|
||||
"models": [(f"{self.app_name}.py", "predefined/apps/generic/models.j2")],
|
||||
"serializers": [(f"{self.app_name}_serializer.py", "predefined/apps/generic/serializers.j2")],
|
||||
"services": [(f"{self.app_name}_service.py", "predefined/apps/generic/services.j2")],
|
||||
"views": [(f"{self.app_name}_view.py", "predefined/apps/generic/views.j2")],
|
||||
"tests": [(f"test_{self.app_name}_api.py", "predefined/apps/generic/tests.j2")],
|
||||
}
|
||||
for folder, files in subfolders.items():
|
||||
folder_path = os.path.join(app_dir, folder)
|
||||
create_directory_with_init(folder_path, f"Created apps/{self.app_name}/{folder}/__init__.py")
|
||||
for filename, template in files:
|
||||
file_path = os.path.join(folder_path, filename)
|
||||
create_file_from_template(
|
||||
file_path, template, base_context, f"Created apps/{self.app_name}/{folder}/{filename}"
|
||||
)
|
||||
|
||||
# urls.py at app root
|
||||
create_file_from_template(
|
||||
os.path.join(app_dir, "urls.py"),
|
||||
"predefined/apps/generic/urls.j2",
|
||||
{"app_name": self.app_name, "app_module": app_module},
|
||||
f"Created apps/{self.app_name}/urls.py",
|
||||
)
|
||||
|
||||
# Add route include to api/v1/urls.py if present
|
||||
self._add_to_api_v1_urls(self.app_name)
|
||||
|
||||
UIFormatter.print_success(f"Created Django app '{self.app_name}' with predefined structure in {apps_dir}")
|
||||
return True
|
||||
|
||||
def _add_to_api_v1_urls(self, app_name: str) -> None:
|
||||
api_v1_urls = os.path.join(self.current_dir, "api", "v1", "urls.py")
|
||||
if not os.path.exists(api_v1_urls):
|
||||
return
|
||||
try:
|
||||
with open(api_v1_urls, encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
import_line = "from django.urls import include, path"
|
||||
if import_line not in content:
|
||||
# ensure base import exists (fallback)
|
||||
content = import_line + "\n\n" + content
|
||||
|
||||
include_stmt = f'path("{app_name}/", include("apps.{app_name}.urls")),'
|
||||
if include_stmt in content:
|
||||
# already present
|
||||
return
|
||||
|
||||
# Insert into urlpatterns list before closing ]
|
||||
if "urlpatterns = [" in content:
|
||||
parts = content.split("urlpatterns = [", 1)
|
||||
before = parts[0]
|
||||
rest = parts[1]
|
||||
if "]" in rest:
|
||||
idx = rest.rfind("]")
|
||||
new_rest = rest[:idx]
|
||||
# ensure newline and indentation
|
||||
if not new_rest.endswith("\n"):
|
||||
new_rest += "\n"
|
||||
new_rest += f" {include_stmt}\n" + rest[idx:]
|
||||
content = before + "urlpatterns = [" + new_rest
|
||||
|
||||
with open(api_v1_urls, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
except Exception:
|
||||
# Non-fatal; leave API routes unchanged if edit fails
|
||||
return
|
||||
@@ -1,41 +0,0 @@
|
||||
"""
|
||||
CI/CD pipeline manager for djinit
|
||||
Handles creation of Github Actions and GitLab CI onfigurations
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from djinit.scripts.template_engine import template_engine
|
||||
from djinit.utils import change_cwd, create_file_with_content
|
||||
|
||||
|
||||
class CICDConfigGenerator:
|
||||
def __init__(self, project_root: str, project_name: str):
|
||||
self.project_root = project_root
|
||||
self.project_name = project_name
|
||||
|
||||
def create_github_actions(self) -> bool:
|
||||
with change_cwd(self.project_root):
|
||||
github_dir = os.path.join(self.project_root, ".github", "workflows")
|
||||
os.makedirs(github_dir, exist_ok=True)
|
||||
|
||||
context = {"project_name": self.project_name}
|
||||
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,
|
||||
workflow_content,
|
||||
"Created Github Actions workflow (ci.yml)",
|
||||
)
|
||||
return True
|
||||
|
||||
def create_gitlab_ci(self) -> bool:
|
||||
with change_cwd(self.project_root):
|
||||
context = {"project_name": self.project_name}
|
||||
gitlab_ci_content = template_engine.render_template("gitlab_ci.j2", context)
|
||||
create_file_with_content(
|
||||
".gitlab-ci.yml",
|
||||
gitlab_ci_content,
|
||||
"Created GitLab CI configuration (.gitlab-ci.yml)",
|
||||
)
|
||||
return True
|
||||
@@ -6,8 +6,13 @@ Handles different commands like secret key generation and app creation.
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
|
||||
from djinit.scripts.app_generator import AppManager
|
||||
from djinit.scripts.console_ui import UIColors, UIFormatter, console
|
||||
from djinit.scripts.name_validator import validate_app_name
|
||||
from djinit.scripts.secretkey_generator import display_secret_keys, generate_multiple_keys
|
||||
|
||||
|
||||
def parse_arguments() -> argparse.Namespace:
|
||||
@@ -43,19 +48,10 @@ def parse_arguments() -> argparse.Namespace:
|
||||
|
||||
|
||||
def handle_secret_command(args: argparse.Namespace) -> None:
|
||||
from djinit.scripts.secretkey_generator import display_secret_keys, generate_multiple_keys
|
||||
|
||||
# Generate keys
|
||||
keys = generate_multiple_keys(args.count, args.length)
|
||||
|
||||
# Display results
|
||||
UIFormatter.print_info("")
|
||||
display_secret_keys(keys)
|
||||
|
||||
# Show usage instructions
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
|
||||
instructions = Text()
|
||||
instructions.append("📋 Usage Instructions:\n", style=UIColors.ACCENT)
|
||||
instructions.append("1. Copy the appropriate secret key for your environment\n")
|
||||
@@ -70,9 +66,6 @@ def handle_secret_command(args: argparse.Namespace) -> None:
|
||||
|
||||
|
||||
def handle_app_command(args: argparse.Namespace) -> None:
|
||||
from djinit.scripts.app_generator import AppManager
|
||||
|
||||
# Display welcome message
|
||||
UIFormatter.print_info("")
|
||||
UIFormatter.print_header("Django App Creation")
|
||||
UIFormatter.print_info("")
|
||||
@@ -103,7 +96,6 @@ def handle_app_command(args: argparse.Namespace) -> None:
|
||||
deduped_names.append(name)
|
||||
seen.add(name)
|
||||
|
||||
# Validate all names first
|
||||
for name in deduped_names:
|
||||
is_valid, error_msg = validate_app_name(name)
|
||||
if not is_valid:
|
||||
@@ -124,16 +116,12 @@ def handle_app_command(args: argparse.Namespace) -> None:
|
||||
if any_failure:
|
||||
sys.exit(1)
|
||||
|
||||
# Show next steps
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
|
||||
instructions = Text()
|
||||
instructions.append("🚀 Next Steps:\n", style=UIColors.ACCENT)
|
||||
instructions.append("1. The app(s) have been added to INSTALLED_APPS in settings/base.py\n")
|
||||
instructions.append("2. Create your models in each app's models.py\n")
|
||||
instructions.append("3. Run migrations: python manage.py makemigrations\n")
|
||||
instructions.append("4. Apply migrations: python manage.py migrate\n")
|
||||
instructions.append("3. Run migrations: just makemigrations\n")
|
||||
instructions.append("4. Apply migrations: just migrate\n")
|
||||
instructions.append("5. Create views, serializers and routes(URLs) for your app(s)\n")
|
||||
|
||||
console.print(Panel(instructions, title="💡 What's Next", border_style="green"))
|
||||
|
||||
@@ -234,6 +234,7 @@ class UIFormatter:
|
||||
app_names: List[str],
|
||||
success: bool,
|
||||
duration: Optional[float] = None,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
"""Create a clean completion summary with optional duration"""
|
||||
console.print("\n")
|
||||
@@ -253,7 +254,18 @@ class UIFormatter:
|
||||
console.print(" [dim]│[/dim]")
|
||||
console.print(f" [dim]├─[/dim] [bold]Directory:[/bold] [white]{project_dir}[/white]")
|
||||
console.print(f" [dim]├─[/dim] [bold]Project:[/bold] [white]{project_name}[/white]")
|
||||
console.print(f" [dim]└─[/dim] [bold]Apps:[/bold] [white]{', '.join(app_names)}[/white]")
|
||||
# Predefined structure adjustments
|
||||
if metadata and metadata.get("predefined_structure"):
|
||||
module_name = metadata.get("project_module_name") or project_name
|
||||
console.print(f" [dim]├─[/dim] [bold]Module:[/bold] [white]{module_name}[/white]")
|
||||
console.print(" [dim]├─[/dim] [bold]Structure:[/bold] [white]Predefined[/white]")
|
||||
# Hide empty apps list in predefined quick flow
|
||||
if app_names:
|
||||
console.print(f" [dim]└─[/dim] [bold]Apps:[/bold] [white]{', '.join(app_names)}[/white]")
|
||||
else:
|
||||
console.print(" [dim]└─[/dim] [bold]Apps:[/bold] [white]users, core[/white]")
|
||||
else:
|
||||
console.print(f" [dim]└─[/dim] [bold]Apps:[/bold] [white]{', '.join(app_names)}[/white]")
|
||||
console.print()
|
||||
|
||||
# Next steps
|
||||
@@ -261,11 +273,9 @@ class UIFormatter:
|
||||
console.print(" [dim]│[/dim]")
|
||||
console.print(" [dim]├─[/dim] [dim]1.[/dim] Navigate to your project directory")
|
||||
console.print(" [dim]├─[/dim] [dim]2.[/dim] Set environment variables in .env file")
|
||||
console.print(" [dim]├─[/dim] [dim]3.[/dim] Run migrations: [cyan]python manage.py migrate[/cyan]")
|
||||
console.print(
|
||||
" [dim]├─[/dim] [dim]4.[/dim] Create superuser: [cyan]python manage.py createsuperuser[/cyan]"
|
||||
)
|
||||
console.print(" [dim]└─[/dim] [dim]5.[/dim] Start server: [cyan]python manage.py runserver[/cyan]")
|
||||
console.print(" [dim]├─[/dim] [dim]3.[/dim] Run migrations: [cyan]just migrate[/cyan]")
|
||||
console.print(" [dim]├─[/dim] [dim]4.[/dim] Create superuser: [cyan]just createsuperuser[/cyan]")
|
||||
console.print(" [dim]└─[/dim] [dim]5.[/dim] Start server: [cyan]just dev[/cyan]")
|
||||
console.print()
|
||||
|
||||
# Useful URLs
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
"""
|
||||
Deployment file generator for djinit.
|
||||
Handles creation of Justfile, Procfile, and deployment-related files.
|
||||
"""
|
||||
|
||||
from djinit.scripts.template_engine import template_engine
|
||||
from djinit.utils import change_cwd, create_file_with_content
|
||||
|
||||
|
||||
class DeploymentConfigGenerator:
|
||||
def __init__(self, project_root: str, project_name: str):
|
||||
self.project_root = project_root
|
||||
self.project_name = project_name
|
||||
|
||||
def create_justfile(self) -> bool:
|
||||
with change_cwd(self.project_root):
|
||||
context = {"project_name": self.project_name}
|
||||
justfile_content = template_engine.render_template("justfile.j2", context)
|
||||
create_file_with_content(
|
||||
"justfile",
|
||||
justfile_content,
|
||||
"Created justfile with Django development tasks",
|
||||
)
|
||||
return True
|
||||
|
||||
def create_procfile(self) -> bool:
|
||||
with change_cwd(self.project_root):
|
||||
context = {"project_name": self.project_name}
|
||||
procfile_content = template_engine.render_template("procfile.j2", context)
|
||||
create_file_with_content(
|
||||
"Procfile",
|
||||
procfile_content,
|
||||
"Created Procfile with Gunicorn configuration",
|
||||
)
|
||||
return True
|
||||
|
||||
def create_gunicorn_config(self) -> bool:
|
||||
with change_cwd(self.project_root):
|
||||
gunicorn_config_content = f"""# Gunicorn configuration file for {self.project_name}
|
||||
# Usage: gunicorn -c gunicorn_config.py {self.project_name}.wsgi:application
|
||||
|
||||
import multiprocessing
|
||||
|
||||
# Server socket
|
||||
bind = "0.0.0.0:8000"
|
||||
backlog = 2048
|
||||
|
||||
# Worker processes
|
||||
workers = multiprocessing.cpu_count() * 2 + 1
|
||||
worker_class = "sync"
|
||||
worker_connections = 1000
|
||||
timeout = 120
|
||||
keepalive = 5
|
||||
|
||||
# Logging
|
||||
accesslog = "-"
|
||||
errorlog = "-"
|
||||
loglevel = "info"
|
||||
access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
|
||||
|
||||
# Process naming
|
||||
proc_name = "{self.project_name}"
|
||||
|
||||
# Server mechanics
|
||||
daemon = False
|
||||
pidfile = None
|
||||
umask = 0
|
||||
user = None
|
||||
group = None
|
||||
tmp_upload_dir = None
|
||||
|
||||
# SSL (uncomment and configure if needed)
|
||||
# keyfile = "/path/to/key.pem"
|
||||
# certfile = "/path/to/cert.pem"
|
||||
"""
|
||||
|
||||
create_file_with_content(
|
||||
"gunicorn_config.py",
|
||||
gunicorn_config_content,
|
||||
"Created gunicorn_config.py with Gunicorn configuration",
|
||||
)
|
||||
return True
|
||||
|
||||
def create_runtime_txt(self, python_version: str = "3.13") -> bool:
|
||||
with change_cwd(self.project_root):
|
||||
context = {"python_version": python_version}
|
||||
runtime_content = template_engine.render_template("runtime_txt.j2", context)
|
||||
create_file_with_content(
|
||||
"runtime.txt",
|
||||
runtime_content,
|
||||
"Created runtime.txt with Python version specification",
|
||||
)
|
||||
return True
|
||||
|
||||
def create_nixpacks_toml(self) -> bool:
|
||||
with change_cwd(self.project_root):
|
||||
nixpacks_content = f"""# Nixpacks configuration for {self.project_name}
|
||||
# Used by Railway, Render, and other platforms that support Nixpacks
|
||||
|
||||
[phases.setup]
|
||||
nixPkgs = ["python311"]
|
||||
|
||||
[phases.install]
|
||||
cmds = ["pip install --upgrade pip", "pip install -r requirements.txt"]
|
||||
|
||||
[phases.build]
|
||||
cmds = ["python manage.py collectstatic --noinput"]
|
||||
|
||||
[start]
|
||||
cmd = "gunicorn {self.project_name}.wsgi:application --bind 0.0.0.0:$PORT"
|
||||
"""
|
||||
|
||||
create_file_with_content(
|
||||
"nixpacks.toml",
|
||||
nixpacks_content,
|
||||
"Created nixpacks.toml for Nixpacks deployment",
|
||||
)
|
||||
return True
|
||||
@@ -0,0 +1,101 @@
|
||||
"""
|
||||
Django helper module that replicates Django's startproject and startapp functionality.
|
||||
|
||||
This module provides functions to create Django projects and apps without requiring Django.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from djinit.scripts.console_ui import UIFormatter
|
||||
from djinit.utils import (
|
||||
create_directory_with_init,
|
||||
create_file_from_template,
|
||||
create_files_from_templates,
|
||||
create_init_file,
|
||||
)
|
||||
|
||||
|
||||
class DjangoHelper:
|
||||
# Django version to use in generated files (matches Django 5.2)
|
||||
DJANGO_VERSION = "5.2"
|
||||
|
||||
@staticmethod
|
||||
def startproject(project_name: str, directory: str, unified: bool = False) -> bool:
|
||||
try:
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
|
||||
# Create manage.py
|
||||
manage_py_path = os.path.join(directory, "manage.py")
|
||||
create_file_from_template(manage_py_path, "shared/manage_py.j2", {}, "Created manage.py")
|
||||
os.chmod(manage_py_path, 0o755)
|
||||
|
||||
# For unified structure, the project config is "core", not project_name
|
||||
# The unified structure is created separately by create_unified_structure
|
||||
if unified:
|
||||
# Just create manage.py for unified, structure is created by create_unified_structure
|
||||
return True
|
||||
|
||||
# Create project config directory
|
||||
project_config_dir = os.path.join(directory, project_name)
|
||||
create_directory_with_init(project_config_dir, f"Created {project_name}/__init__.py")
|
||||
|
||||
# Create settings directory and files
|
||||
settings_dir = os.path.join(project_config_dir, "settings")
|
||||
create_directory_with_init(settings_dir, f"Created {project_name}/settings/__init__.py")
|
||||
|
||||
base_context = {"project_name": project_name, "app_names": []}
|
||||
settings_files = [
|
||||
("base.py", "project/settings/base.j2", base_context),
|
||||
("development.py", "project/settings/development.j2", {}),
|
||||
("production.py", "project/settings/production.j2", {}),
|
||||
]
|
||||
create_files_from_templates(settings_dir, settings_files, f"{project_name}/settings/")
|
||||
|
||||
# Create project-level files
|
||||
urls_context = {"project_name": project_name, "django_version": DjangoHelper.DJANGO_VERSION}
|
||||
project_files = [
|
||||
("urls.py", "project/urls.j2", urls_context),
|
||||
("wsgi.py", "project/wsgi.j2", {}),
|
||||
("asgi.py", "project/asgi.j2", {}),
|
||||
]
|
||||
create_files_from_templates(project_config_dir, project_files, f"{project_name}/")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
UIFormatter.print_error(f"Error creating Django project: {e}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def startapp(app_name: str, directory: str) -> bool:
|
||||
try:
|
||||
app_dir = os.path.join(directory, app_name)
|
||||
os.makedirs(app_dir, exist_ok=True)
|
||||
|
||||
context = {"app_name": app_name, "django_version": DjangoHelper.DJANGO_VERSION}
|
||||
|
||||
# Create app files
|
||||
create_init_file(app_dir, f"Created {app_name}/__init__.py")
|
||||
|
||||
app_files = [
|
||||
("apps.py", "base/apps.j2", context),
|
||||
("models.py", "base/models.j2", context),
|
||||
("views.py", "base/views.j2", context),
|
||||
("admin.py", "base/admin.j2", context),
|
||||
("urls.py", "base/urls.j2", context),
|
||||
("serializers.py", "base/serializers.j2", context),
|
||||
("routes.py", "base/routes.j2", context),
|
||||
("tests.py", "base/tests.j2", context),
|
||||
]
|
||||
|
||||
create_files_from_templates(app_dir, app_files, f"{app_name}/")
|
||||
|
||||
# Create migrations directory and __init__.py
|
||||
migrations_dir = os.path.join(app_dir, "migrations")
|
||||
create_directory_with_init(migrations_dir, f"Created {app_name}/migrations/__init__.py")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
UIFormatter.print_error(f"Error creating Django app: {e}")
|
||||
return False
|
||||
+305
-124
@@ -6,7 +6,15 @@ Handles creation and modification of project files.
|
||||
import os
|
||||
|
||||
from djinit.scripts.template_engine import template_engine
|
||||
from djinit.utils import change_cwd, create_file_with_content
|
||||
from djinit.utils import (
|
||||
calculate_app_module_paths,
|
||||
change_cwd,
|
||||
create_directory_with_init,
|
||||
create_file_from_template,
|
||||
create_file_with_content,
|
||||
create_files_from_templates,
|
||||
get_package_name,
|
||||
)
|
||||
|
||||
|
||||
class FileManager:
|
||||
@@ -15,149 +23,322 @@ class FileManager:
|
||||
self.project_name = project_name
|
||||
self.app_names = app_names
|
||||
self.metadata = metadata
|
||||
self.project_configs = os.path.join(project_root, project_name)
|
||||
# Use module name (e.g., 'config') for Django package paths if provided
|
||||
# Fall back to project_name if key present but None
|
||||
self.module_name = metadata.get("project_module_name") or project_name
|
||||
self.project_configs = os.path.join(project_root, self.module_name)
|
||||
self.settings_folder = os.path.join(self.project_configs, "settings")
|
||||
self.settings_file = os.path.join(self.project_configs, "settings.py")
|
||||
|
||||
def _render_and_create_file(
|
||||
self,
|
||||
filepath: str,
|
||||
template_path: str,
|
||||
context: dict,
|
||||
message: str,
|
||||
base_dir: str = None,
|
||||
should_format: bool = False,
|
||||
) -> bool:
|
||||
"""Helper to render template and create file."""
|
||||
target_dir = base_dir or self.project_root
|
||||
with change_cwd(target_dir):
|
||||
content = template_engine.render_template(template_path, context)
|
||||
return create_file_with_content(filepath, content, message, should_format=should_format)
|
||||
|
||||
def _create_files_from_specs(self, base_dir: str, folder_specs: dict, base_context: dict = None) -> None:
|
||||
"""Helper to create multiple folders and files from a specification dict."""
|
||||
base_context = base_context or {}
|
||||
for folder, files in folder_specs.items():
|
||||
folder_path = os.path.join(base_dir, folder)
|
||||
create_directory_with_init(folder_path, f"Created {folder_path}/__init__.py")
|
||||
for filename, template_path in files:
|
||||
file_path = os.path.join(folder_path, filename)
|
||||
create_file_from_template(file_path, template_path, base_context, f"Created {file_path}")
|
||||
|
||||
def _create_apps_py(self, app_dir: str, app_name: str, message: str = None, should_format: bool = False) -> None:
|
||||
"""Helper to create apps.py file for an app."""
|
||||
apps_py_path = os.path.join(app_dir, "apps.py")
|
||||
context = {"app_name": app_name} if app_name else {}
|
||||
msg = message or f"Created {os.path.basename(app_dir)}/apps.py"
|
||||
create_file_from_template(apps_py_path, "base/apps.j2", context, msg, should_format=should_format)
|
||||
|
||||
def _create_subdirectories_with_init(self, base_dir: str, subdirs: list, prefix: str = "") -> None:
|
||||
"""Helper to create multiple subdirectories with __init__.py files."""
|
||||
for subdir in subdirs:
|
||||
subdir_path = os.path.join(base_dir, subdir)
|
||||
message = f"Created {prefix}{subdir}/__init__.py" if prefix else f"Created {subdir}/__init__.py"
|
||||
create_directory_with_init(subdir_path, message)
|
||||
|
||||
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",
|
||||
)
|
||||
return self._render_and_create_file(".gitignore", "shared/gitignore.j2", {}, "Created .gitignore file")
|
||||
|
||||
def create_requirements(self) -> bool:
|
||||
with change_cwd(self.project_root):
|
||||
context = {"use_database_url": self.metadata.get("use_database_url", True)}
|
||||
requirements_content = template_engine.render_template("requirements.j2", context)
|
||||
return create_file_with_content(
|
||||
"requirements.txt",
|
||||
requirements_content,
|
||||
"Created requirements.txt with Django dependencies",
|
||||
)
|
||||
context = {"use_database_url": self.metadata.get("use_database_url", True)}
|
||||
return self._render_and_create_file(
|
||||
"requirements.txt",
|
||||
"shared/requirements.j2",
|
||||
context,
|
||||
"Created requirements.txt with Django dependencies",
|
||||
)
|
||||
|
||||
def create_readme(self) -> bool:
|
||||
with change_cwd(self.project_root):
|
||||
context = {"project_name": self.project_name, "app_names": self.app_names}
|
||||
readme_content = template_engine.render_template("readme.j2", context)
|
||||
return create_file_with_content(
|
||||
"README.md",
|
||||
readme_content,
|
||||
"Created README.md file",
|
||||
)
|
||||
context = {
|
||||
"project_name": self.project_name,
|
||||
"app_names": self.app_names,
|
||||
"predefined_structure": bool(self.metadata.get("predefined_structure")),
|
||||
"unified_structure": bool(self.metadata.get("unified_structure")),
|
||||
"module_name": self.module_name,
|
||||
}
|
||||
return self._render_and_create_file("README.md", "shared/readme.j2", context, "Created README.md file")
|
||||
|
||||
def create_env_file(self) -> bool:
|
||||
"""Create .env.sample file with environment variables."""
|
||||
with change_cwd(self.project_root):
|
||||
context = {
|
||||
"project_name": self.project_name,
|
||||
"use_database_url": self.metadata.get("use_database_url", True),
|
||||
}
|
||||
env_content = template_engine.render_template("env_sample.j2", context)
|
||||
return create_file_with_content(
|
||||
".env.sample",
|
||||
env_content,
|
||||
"Created .env.sample file with generated secret key",
|
||||
)
|
||||
context = {
|
||||
"project_name": self.project_name,
|
||||
"use_database_url": self.metadata.get("use_database_url", True),
|
||||
}
|
||||
return self._render_and_create_file(
|
||||
".env.sample",
|
||||
"shared/env_sample.j2",
|
||||
context,
|
||||
"Created .env.sample file with generated secret key",
|
||||
)
|
||||
|
||||
def create_pyproject_toml(self, metadata: dict) -> bool:
|
||||
with change_cwd(self.project_root):
|
||||
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",
|
||||
)
|
||||
# Default package_name to "backend" if it's "." or empty
|
||||
package_name = metadata.get("package_name", "backend")
|
||||
package_name = get_package_name(package_name)
|
||||
context = {"package_name": package_name, "project_name": self.project_name}
|
||||
return self._render_and_create_file(
|
||||
"pyproject.toml",
|
||||
"shared/pyproject_toml.j2",
|
||||
context,
|
||||
"Created pyproject.toml file for uv",
|
||||
)
|
||||
|
||||
def _get_apps_base_dir(self) -> str:
|
||||
"""Get the base directory for apps based on project structure."""
|
||||
if self.metadata.get("nested_apps") and self.metadata.get("nested_dir"):
|
||||
return os.path.join(self.project_root, self.metadata.get("nested_dir"))
|
||||
return self.project_root
|
||||
def create_project_urls(self) -> bool:
|
||||
"""Create project urls.py using project_urls.j2 template (replaces update_project_urls)."""
|
||||
effective_app_modules = calculate_app_module_paths(self.app_names, self.metadata)
|
||||
context = {
|
||||
"project_name": self.project_name,
|
||||
"app_names": effective_app_modules,
|
||||
}
|
||||
return self._render_and_create_file(
|
||||
"urls.py",
|
||||
"project/urls_with_apps.j2",
|
||||
context,
|
||||
"Created project urls.py with comprehensive URL configuration",
|
||||
base_dir=self.project_configs,
|
||||
should_format=True,
|
||||
)
|
||||
|
||||
def _create_app_file(self, apps_base_dir: str, app_name: str, template_name: str, filename: str) -> None:
|
||||
"""Helper method to create app files."""
|
||||
app_path = os.path.join(apps_base_dir, app_name)
|
||||
with change_cwd(app_path):
|
||||
context = {"app_name": app_name}
|
||||
content = template_engine.render_template(template_name, context)
|
||||
create_file_with_content(
|
||||
filename,
|
||||
content,
|
||||
f"Created {app_name}/{filename}",
|
||||
should_format=True,
|
||||
)
|
||||
def create_justfile(self) -> bool:
|
||||
context = {"project_name": self.project_name}
|
||||
return self._render_and_create_file(
|
||||
"justfile",
|
||||
"shared/justfile.j2",
|
||||
context,
|
||||
"Created justfile with Django development tasks",
|
||||
)
|
||||
|
||||
def create_predefined_structure(self) -> bool:
|
||||
# Ensure apps package
|
||||
apps_dir = os.path.join(self.project_root, "apps")
|
||||
create_directory_with_init(apps_dir, "Created apps/__init__.py")
|
||||
|
||||
# users app
|
||||
users_dir = os.path.join(apps_dir, "users")
|
||||
create_directory_with_init(users_dir, "Created apps/users/__init__.py")
|
||||
self._create_apps_py(users_dir, "users")
|
||||
|
||||
# Nested folders and files for users
|
||||
users_subfolders = {
|
||||
"models": [("user.py", "predefined/apps/users/models.j2")],
|
||||
"serializers": [("user_serializer.py", "predefined/apps/users/serializers.j2")],
|
||||
"services": [("user_service.py", "predefined/apps/users/services.j2")],
|
||||
"views": [("user_view.py", "predefined/apps/users/views.j2")],
|
||||
"tests": [("test_user_api.py", "predefined/apps/users/tests.j2")],
|
||||
}
|
||||
self._create_files_from_specs(users_dir, users_subfolders, {"app_module": "apps.users"})
|
||||
|
||||
# users urls.py at app root
|
||||
users_urls_path = os.path.join(users_dir, "urls.py")
|
||||
create_file_from_template(
|
||||
users_urls_path, "predefined/apps/users/urls.j2", {"app_module": "apps.users"}, "Created apps/users/urls.py"
|
||||
)
|
||||
|
||||
# core app
|
||||
core_dir = os.path.join(apps_dir, "core")
|
||||
create_directory_with_init(core_dir, "Created apps/core/__init__.py")
|
||||
|
||||
# core subfolders and files
|
||||
core_subfolders = {
|
||||
"utils": [("responses.py", "predefined/core/utils/responses.j2")],
|
||||
"mixins": [("timestamped_model.py", "predefined/core/mixins/timestamped_model.j2")],
|
||||
"middleware": [("request_logger.py", "predefined/core/middleware/request_logger.j2")],
|
||||
}
|
||||
self._create_files_from_specs(core_dir, core_subfolders, {})
|
||||
|
||||
# core/exceptions.py
|
||||
exceptions_path = os.path.join(core_dir, "exceptions.py")
|
||||
create_file_from_template(
|
||||
exceptions_path, "predefined/core/exceptions.j2", {}, "Created apps/core/exceptions.py"
|
||||
)
|
||||
|
||||
# api package
|
||||
api_dir = os.path.join(self.project_root, "api")
|
||||
create_directory_with_init(api_dir, "Created api/__init__.py")
|
||||
api_urls_path = os.path.join(api_dir, "urls.py")
|
||||
create_file_from_template(api_urls_path, "predefined/api/urls.j2", {}, "Created api/urls.py")
|
||||
|
||||
# api/v1
|
||||
api_v1_dir = os.path.join(api_dir, "v1")
|
||||
create_directory_with_init(api_v1_dir, "Created api/v1/__init__.py")
|
||||
api_v1_urls_path = os.path.join(api_v1_dir, "urls.py")
|
||||
create_file_from_template(api_v1_urls_path, "predefined/api/v1/urls.j2", {}, "Created api/v1/urls.py")
|
||||
|
||||
# overwrite project urls to include api
|
||||
project_urls_path = os.path.join(self.project_configs, "urls.py")
|
||||
create_file_from_template(
|
||||
project_urls_path,
|
||||
"project/urls_with_api.j2",
|
||||
{"api_module": "api"},
|
||||
"Updated project urls.py to include API routes",
|
||||
should_format=True,
|
||||
)
|
||||
|
||||
def create_app_urls(self) -> bool:
|
||||
apps_base_dir = self._get_apps_base_dir()
|
||||
for app_name in self.app_names:
|
||||
self._create_app_file(apps_base_dir, app_name, "app_urls.j2", "urls.py")
|
||||
return True
|
||||
|
||||
def create_app_serializers(self) -> bool:
|
||||
"""Create serializers.py file for each app."""
|
||||
apps_base_dir = self._get_apps_base_dir()
|
||||
for app_name in self.app_names:
|
||||
self._create_app_file(apps_base_dir, app_name, "serializers.j2", "serializers.py")
|
||||
def create_unified_structure(self) -> bool:
|
||||
# Create core directory structure
|
||||
core_dir = os.path.join(self.project_root, "core")
|
||||
create_directory_with_init(core_dir, "Created core/__init__.py")
|
||||
|
||||
# Create core/settings directory
|
||||
settings_dir = os.path.join(core_dir, "settings")
|
||||
create_directory_with_init(settings_dir, "Created core/settings/__init__.py")
|
||||
|
||||
# Generate secret key for development settings
|
||||
from djinit.scripts.secretkey_generator import generate_secret_key
|
||||
|
||||
secret_key = generate_secret_key()
|
||||
|
||||
# Create settings files using project templates with unified context
|
||||
# For unified structure, project_module_name is "core"
|
||||
base_context = {
|
||||
"project_name": "core", # Use "core" as the module name
|
||||
"app_names": ["apps.core", "apps.api"] + [f"apps.{app}" for app in self.app_names],
|
||||
"use_database_url": self.metadata.get("use_database_url", True),
|
||||
}
|
||||
dev_context = {"secret_key": secret_key}
|
||||
|
||||
# Settings files
|
||||
for filename, context in [
|
||||
("base.py", base_context),
|
||||
("development.py", dev_context),
|
||||
("production.py", base_context),
|
||||
]:
|
||||
filepath = os.path.join(settings_dir, filename)
|
||||
template_path = f"project/settings/{filename.replace('.py', '')}.j2"
|
||||
create_file_from_template(
|
||||
filepath, template_path, context, f"Created core/settings/{filename}", should_format=True
|
||||
)
|
||||
|
||||
# Core project files
|
||||
core_files = [
|
||||
("urls.py", "project/urls_with_api.j2", {"api_module": "apps.api"}),
|
||||
("wsgi.py", "project/wsgi.j2", {}),
|
||||
("asgi.py", "project/asgi.j2", {}),
|
||||
]
|
||||
for filename, template, context in core_files:
|
||||
filepath = os.path.join(core_dir, filename)
|
||||
create_file_from_template(filepath, template, context, f"Created core/{filename}", should_format=True)
|
||||
|
||||
# Create apps directory
|
||||
apps_dir = os.path.join(self.project_root, "apps")
|
||||
create_directory_with_init(apps_dir, "Created apps/__init__.py")
|
||||
|
||||
# Create apps/core
|
||||
apps_core_dir = os.path.join(apps_dir, "core")
|
||||
create_directory_with_init(apps_core_dir, "Created apps/core/__init__.py")
|
||||
|
||||
# Create apps/core/apps.py
|
||||
self._create_apps_py(apps_core_dir, "", "Created apps/core/apps.py", should_format=True)
|
||||
|
||||
# Create apps/core/models
|
||||
models_dir = os.path.join(apps_core_dir, "models")
|
||||
model_files = [
|
||||
("__init__.py", "base/models.j2"),
|
||||
("base.py", "unified/apps/core/models/base.py.j2"),
|
||||
("mixins.py", "unified/apps/core/models/mixins.py.j2"),
|
||||
]
|
||||
os.makedirs(models_dir, exist_ok=True)
|
||||
create_files_from_templates(
|
||||
models_dir, [(f, t, {}) for f, t in model_files], "apps/core/models/", should_format=True
|
||||
)
|
||||
|
||||
# Create apps/core/utils
|
||||
utils_dir = os.path.join(apps_core_dir, "utils")
|
||||
utils_files = [
|
||||
("__init__.py", "unified/apps/core/utils/__init__.py.j2"),
|
||||
("responses.py", "unified/apps/core/utils/responses.py.j2"),
|
||||
]
|
||||
os.makedirs(utils_dir, exist_ok=True)
|
||||
create_files_from_templates(
|
||||
utils_dir, [(f, t, {}) for f, t in utils_files], "apps/core/utils/", should_format=True
|
||||
)
|
||||
|
||||
# Create apps/core/permissions and middleware
|
||||
self._create_subdirectories_with_init(apps_core_dir, ["permissions", "middleware"], "apps/core/")
|
||||
|
||||
# Create apps/api
|
||||
apps_api_dir = os.path.join(apps_dir, "api")
|
||||
create_directory_with_init(apps_api_dir, "Created apps/api/__init__.py")
|
||||
self._create_apps_py(apps_api_dir, "apps.api", "Created apps/api/apps.py", should_format=True)
|
||||
|
||||
# Create apps/api subdirectories
|
||||
self._create_subdirectories_with_init(
|
||||
apps_api_dir, ["models", "serializers", "views", "services", "tests", "urls", "admin"], "apps/api/"
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
def create_app_routes(self) -> bool:
|
||||
"""Create routes.py file for each app."""
|
||||
apps_base_dir = self._get_apps_base_dir()
|
||||
for app_name in self.app_names:
|
||||
self._create_app_file(apps_base_dir, app_name, "routes.j2", "routes.py")
|
||||
return True
|
||||
def create_procfile(self) -> bool:
|
||||
context = {"project_name": self.project_name}
|
||||
return self._render_and_create_file(
|
||||
"Procfile",
|
||||
"shared/procfile.j2",
|
||||
context,
|
||||
"Created Procfile with Gunicorn configuration",
|
||||
)
|
||||
|
||||
def 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 create_runtime_txt(self) -> bool:
|
||||
context = {"python_version": "3.13"}
|
||||
return self._render_and_create_file(
|
||||
"runtime.txt",
|
||||
"shared/runtime_txt.j2",
|
||||
context,
|
||||
"Created runtime.txt with Python version specification",
|
||||
)
|
||||
|
||||
def 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 create_github_actions(self) -> bool:
|
||||
github_dir = os.path.join(self.project_root, ".github", "workflows")
|
||||
os.makedirs(github_dir, exist_ok=True)
|
||||
context = {"project_name": self.project_name, "module_name": self.module_name}
|
||||
workflow_file = os.path.join(github_dir, "ci.yml")
|
||||
return self._render_and_create_file(
|
||||
workflow_file,
|
||||
"shared/ci/github_actions.j2",
|
||||
context,
|
||||
"Created Github Actions workflow (ci.yml)",
|
||||
base_dir=self.project_root,
|
||||
)
|
||||
|
||||
def 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,
|
||||
)
|
||||
def create_gitlab_ci(self) -> bool:
|
||||
context = {"project_name": self.project_name, "module_name": self.module_name}
|
||||
return self._render_and_create_file(
|
||||
".gitlab-ci.yml",
|
||||
"shared/ci/gitlab_ci.j2",
|
||||
context,
|
||||
"Created GitLab CI configuration (.gitlab-ci.yml)",
|
||||
)
|
||||
+312
-137
@@ -13,6 +13,7 @@ from typing import Callable, Tuple
|
||||
|
||||
from djinit.scripts.console_ui import UIColors, UIFormatter, console
|
||||
from djinit.scripts.name_validator import validate_app_name, validate_project_name
|
||||
from djinit.utils import get_package_name
|
||||
|
||||
|
||||
class CICDOption(Enum):
|
||||
@@ -34,9 +35,12 @@ class ProjectMetadata:
|
||||
nested_apps: bool = False
|
||||
nested_dir: str | None = None
|
||||
use_database_url: bool = False
|
||||
# predefined structure support
|
||||
predefined_structure: bool = False
|
||||
unified_structure: bool = False
|
||||
project_module_name: str | None = None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""Convert to dictionary format."""
|
||||
return {
|
||||
"package_name": self.package_name,
|
||||
"use_github_actions": self.use_github_actions,
|
||||
@@ -44,13 +48,14 @@ class ProjectMetadata:
|
||||
"nested_apps": self.nested_apps,
|
||||
"nested_dir": self.nested_dir,
|
||||
"use_database_url": self.use_database_url,
|
||||
"predefined_structure": self.predefined_structure,
|
||||
"unified_structure": self.unified_structure,
|
||||
"project_module_name": self.project_module_name,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProjectSetup:
|
||||
"""Complete project setup configuration."""
|
||||
|
||||
project_dir: str
|
||||
project_name: str
|
||||
primary_app: str
|
||||
@@ -58,17 +63,14 @@ class ProjectSetup:
|
||||
metadata: ProjectMetadata
|
||||
|
||||
def to_tuple(self) -> Tuple[str, str, str, list, dict]:
|
||||
"""Convert to legacy tuple format for backward compatibility."""
|
||||
return (self.project_dir, self.project_name, self.primary_app, self.app_names, self.metadata.to_dict())
|
||||
|
||||
|
||||
class InputCollector:
|
||||
"""Handles collection of user inputs with validation."""
|
||||
|
||||
MAX_ATTEMPTS = 3
|
||||
|
||||
def __init__(self):
|
||||
self.char_reader = CharReader()
|
||||
pass
|
||||
|
||||
def get_validated_input(
|
||||
self, prompt: str, validator: Callable[[str], Tuple[bool, str]], input_type: str, allow_empty: bool = False
|
||||
@@ -107,41 +109,42 @@ class InputCollector:
|
||||
UIFormatter.print_error(f"Maximum attempts reached for {input_type}. Exiting.")
|
||||
sys.exit(1)
|
||||
|
||||
def _show_retry_message(self, attempt: int) -> None:
|
||||
if attempt < self.MAX_ATTEMPTS:
|
||||
console.print(f"[{UIColors.MUTED}]Please try again ({attempt}/{self.MAX_ATTEMPTS}).[/{UIColors.MUTED}]")
|
||||
def _show_retry_message(self, attempt: int, max_attempts: int = None) -> None:
|
||||
max_attempts = max_attempts or self.MAX_ATTEMPTS
|
||||
if attempt < max_attempts:
|
||||
console.print(f"[{UIColors.MUTED}]Please try again ({attempt}/{max_attempts}).[/{UIColors.MUTED}]")
|
||||
|
||||
def get_app_names(self) -> list[str]:
|
||||
console.print(f"[{UIColors.HIGHLIGHT}]App Names[/{UIColors.HIGHLIGHT}]")
|
||||
console.print(
|
||||
f"[{UIColors.MUTED}]Enter app names separated by commas (no interactive prompts)[/{UIColors.MUTED}]"
|
||||
f"[{UIColors.MUTED}]Enter app names separated by commas(e.g. users, products, orders)[/{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 single):[/{UIColors.HIGHLIGHT}] "
|
||||
)
|
||||
|
||||
# Empty input - re-prompt (no interactive flow)
|
||||
if not user_input.strip():
|
||||
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 (no additional prompts)
|
||||
return self._get_apps_starting_with(user_input.strip())
|
||||
|
||||
def _parse_comma_separated_apps(self, user_input: str) -> list[str]:
|
||||
app_list = [app.strip() for app in user_input.split(",")]
|
||||
app_list = [app for app in app_list if app] # Remove empty strings
|
||||
app_list = [app.strip() for app in user_input.split(",") if app.strip()]
|
||||
|
||||
if not app_list:
|
||||
UIFormatter.print_error("At least one app name is required")
|
||||
return self.get_app_names()
|
||||
|
||||
return self._validate_app_list(app_list)
|
||||
|
||||
def _get_apps_starting_with(self, first_app: str) -> list[str]:
|
||||
return self._validate_app_list([first_app])
|
||||
|
||||
def _validate_app_list(self, app_list: list[str]) -> list[str]:
|
||||
invalid_apps = []
|
||||
for app in app_list:
|
||||
is_valid, error_msg = validate_app_name(app)
|
||||
@@ -155,102 +158,132 @@ class InputCollector:
|
||||
|
||||
return app_list
|
||||
|
||||
def _get_apps_starting_with(self, first_app: str) -> list[str]:
|
||||
is_valid, error_msg = validate_app_name(first_app)
|
||||
if not is_valid:
|
||||
UIFormatter.print_error(error_msg)
|
||||
return self.get_app_names()
|
||||
def _get_project_directory(self) -> str | None:
|
||||
attempt = 0
|
||||
|
||||
return [first_app]
|
||||
while attempt < self.MAX_ATTEMPTS:
|
||||
try:
|
||||
user_input = console.input(
|
||||
f"[{UIColors.HIGHLIGHT}]Enter project directory name:[/{UIColors.HIGHLIGHT}] "
|
||||
).strip()
|
||||
|
||||
def _get_apps_interactive(self) -> list[str]:
|
||||
"""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()
|
||||
# Handle empty input or '.' - use current directory
|
||||
if not user_input or user_input == ".":
|
||||
UIFormatter.print_info(f"Creating project in current directory: {os.getcwd()}")
|
||||
return "."
|
||||
|
||||
def _prompt_for_additional_apps(self) -> list[str]:
|
||||
"""Deprecated: interactive flow disabled. Always returns empty list."""
|
||||
return []
|
||||
is_valid, error_msg = validate_project_name(user_input)
|
||||
if not is_valid:
|
||||
UIFormatter.print_error(error_msg)
|
||||
attempt += 1
|
||||
self._show_retry_message(attempt)
|
||||
continue
|
||||
|
||||
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
|
||||
self._show_retry_message(attempt)
|
||||
continue
|
||||
|
||||
return user_input
|
||||
|
||||
except KeyboardInterrupt:
|
||||
UIFormatter.print_info("\nSetup cancelled by user.")
|
||||
sys.exit(0)
|
||||
|
||||
return None
|
||||
|
||||
def get_cicd_choice(self) -> Tuple[bool, bool]:
|
||||
UIFormatter.print_separator()
|
||||
console.print(f"\n[{UIColors.INFO}]Step 3: CI/CD Pipeline[/{UIColors.INFO}]\n")
|
||||
console.print(f"[{UIColors.MUTED}]Choose CI/CD pipeline for your project[/{UIColors.MUTED}]")
|
||||
console.print()
|
||||
|
||||
# Show options
|
||||
for option in CICDOption:
|
||||
console.print(f" [{UIColors.SUCCESS}]{option.key}[/{UIColors.SUCCESS}] {option.description}")
|
||||
console.print()
|
||||
|
||||
console.print(f"[{UIColors.HIGHLIGHT}]Your choice: [/{UIColors.HIGHLIGHT}]", end="")
|
||||
sys.stdout.flush()
|
||||
valid_keys = [option.key for option in CICDOption]
|
||||
|
||||
choice = self.char_reader.get_char()
|
||||
console.print(choice.upper())
|
||||
choice = CharReader.get_cicd_choice(
|
||||
f"[{UIColors.HIGHLIGHT}]Your choice: [/{UIColors.HIGHLIGHT}]",
|
||||
valid_keys=valid_keys,
|
||||
default=CICDOption.NONE.key,
|
||||
)
|
||||
|
||||
# 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 for any unrecognized input
|
||||
return False, False
|
||||
choice_map = {
|
||||
CICDOption.BOTH.key: (True, True),
|
||||
CICDOption.GITHUB.key: (True, False),
|
||||
CICDOption.GITLAB.key: (False, True),
|
||||
}
|
||||
return choice_map.get(choice, (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}]"
|
||||
f"[{UIColors.MUTED}]Do you want to place apps inside a package directory (e.g. 'src/')?[/{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())
|
||||
choice = CharReader.get_yes_no(f"[{UIColors.HIGHLIGHT}]Nested Django apps? (y/N):[/{UIColors.HIGHLIGHT}]")
|
||||
|
||||
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)",
|
||||
"Enter directory name for apps package (e.g. src)",
|
||||
validate_project_name,
|
||||
"apps package name",
|
||||
)
|
||||
return True, dir_name
|
||||
|
||||
def get_database_config_choice(self) -> bool:
|
||||
"""Ask user for database configuration preference."""
|
||||
UIFormatter.print_separator()
|
||||
console.print(f"\n[{UIColors.INFO}]Database Configuration[/{UIColors.INFO}]\n")
|
||||
console.print(f"[{UIColors.MUTED}]Choose how to configure your database in production:[/{UIColors.MUTED}]")
|
||||
console.print()
|
||||
console.print(f" [{UIColors.SUCCESS}]Y[/{UIColors.SUCCESS}] Use DATABASE_URL (recommended for production)")
|
||||
console.print(f" [{UIColors.SUCCESS}]N[/{UIColors.SUCCESS}] Use individual database parameters")
|
||||
console.print()
|
||||
console.print(f"[{UIColors.MUTED}]DATABASE_URL is a single environment variable like:[/{UIColors.MUTED}]")
|
||||
console.print(f"[{UIColors.MUTED}]postgres://user:password@host:port/database[/{UIColors.MUTED}]")
|
||||
console.print()
|
||||
|
||||
console.print(f"[{UIColors.HIGHLIGHT}]Use DATABASE_URL? (Y/n):[/{UIColors.HIGHLIGHT}] ", end="")
|
||||
sys.stdout.flush()
|
||||
choice = self.char_reader.get_char()
|
||||
console.print(choice.upper())
|
||||
return CharReader.get_yes_no(f"[{UIColors.HIGHLIGHT}]Use DATABASE_URL? (Y/n):[/{UIColors.HIGHLIGHT}]") == "y"
|
||||
|
||||
# Default to True (use DATABASE_URL) if user presses Enter or Y
|
||||
return choice.lower() != "n"
|
||||
def _get_structure_metadata(
|
||||
self, project_dir: str, predefined: bool = False, unified: bool = False
|
||||
) -> Tuple[str, str, list[str], dict]:
|
||||
"""Helper method to get metadata for predefined/unified structures."""
|
||||
project_name = project_dir
|
||||
app_names: list[str] = []
|
||||
|
||||
# Ask for workflows so shared templates for CI/CD can be added
|
||||
use_github, use_gitlab = self.get_cicd_choice()
|
||||
|
||||
# Ask database configuration
|
||||
use_database_url = self.get_database_config_choice()
|
||||
|
||||
# Default package_name to "backend" if project_dir is "." or empty
|
||||
package_name = get_package_name(project_dir)
|
||||
|
||||
project_module_name = "config" if predefined else "core" if unified else None
|
||||
|
||||
metadata = ProjectMetadata(
|
||||
package_name=package_name,
|
||||
use_github_actions=use_github,
|
||||
use_gitlab_ci=use_gitlab,
|
||||
nested_apps=True,
|
||||
nested_dir="apps",
|
||||
use_database_url=use_database_url,
|
||||
predefined_structure=predefined,
|
||||
unified_structure=unified,
|
||||
project_module_name=project_module_name,
|
||||
)
|
||||
|
||||
return project_name, "", app_names, metadata.to_dict()
|
||||
|
||||
|
||||
class CharReader:
|
||||
"""Handles single character input across platforms."""
|
||||
MAX_ATTEMPTS = 3
|
||||
|
||||
@staticmethod
|
||||
def get_char() -> str:
|
||||
@@ -258,6 +291,110 @@ class CharReader:
|
||||
return CharReader._get_char_windows()
|
||||
return CharReader._get_char_unix()
|
||||
|
||||
@staticmethod
|
||||
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
|
||||
|
||||
@staticmethod
|
||||
def _get_char_input(prompt: str) -> str:
|
||||
console.print(f"{prompt} ", end="")
|
||||
sys.stdout.flush()
|
||||
return CharReader().get_char()
|
||||
|
||||
@staticmethod
|
||||
def _handle_default(default: str) -> str:
|
||||
console.print(default.upper())
|
||||
return default.lower()
|
||||
|
||||
@staticmethod
|
||||
def _get_validated_char_input(
|
||||
prompt: str,
|
||||
valid_keys: list[str],
|
||||
default: str,
|
||||
error_message: str,
|
||||
) -> str:
|
||||
attempt = 0
|
||||
valid_keys_lower = [k.lower() for k in valid_keys]
|
||||
|
||||
while attempt < CharReader.MAX_ATTEMPTS:
|
||||
try:
|
||||
response = CharReader._get_char_input(prompt)
|
||||
|
||||
# Handle Enter key (default)
|
||||
if CharReader._is_enter_key(response):
|
||||
return CharReader._handle_default(default)
|
||||
|
||||
response = response.lower()
|
||||
|
||||
# Validate response
|
||||
if response in valid_keys_lower:
|
||||
console.print(response.upper())
|
||||
return response
|
||||
|
||||
# Invalid input - show error
|
||||
console.print()
|
||||
if response.isprintable():
|
||||
UIFormatter.print_error(error_message.format(response=response))
|
||||
attempt += 1
|
||||
if attempt < CharReader.MAX_ATTEMPTS:
|
||||
console.print(
|
||||
f"[{UIColors.MUTED}]Please try again ({attempt}/{CharReader.MAX_ATTEMPTS}).[/{UIColors.MUTED}]"
|
||||
)
|
||||
console.print()
|
||||
else:
|
||||
# Control character entered, treat as Enter
|
||||
return CharReader._handle_default(default)
|
||||
|
||||
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()
|
||||
|
||||
@staticmethod
|
||||
def get_yes_no(prompt: str, default: str = None) -> str:
|
||||
# Auto-detect default from prompt format (capital letter indicates default)
|
||||
if default is None:
|
||||
if "(Y/n)" in prompt or "(Y/N)" in prompt:
|
||||
default = "y"
|
||||
elif "(y/N)" in prompt:
|
||||
default = "n"
|
||||
else:
|
||||
# Default to 'y' for yes/no prompts
|
||||
default = "y"
|
||||
|
||||
return CharReader._get_validated_char_input(
|
||||
prompt=prompt,
|
||||
valid_keys=["y", "n"],
|
||||
default=default,
|
||||
error_message="Invalid input '{response}'. Please enter 'y' or 'n'.",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_cicd_choice(prompt: str, valid_keys: list[str], default: str = "n") -> str:
|
||||
valid_keys_str = ", ".join(f"'{k}'" for k in valid_keys)
|
||||
return CharReader._get_validated_char_input(
|
||||
prompt=prompt,
|
||||
valid_keys=valid_keys,
|
||||
default=default,
|
||||
error_message=f"Invalid input '{{response}}'. Please enter one of: {valid_keys_str}.",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_structure_choice() -> str:
|
||||
"""Get structure type choice without requiring Enter key."""
|
||||
return CharReader._get_validated_char_input(
|
||||
prompt=f"[{UIColors.HIGHLIGHT}]Choose structure type (1/2/3) [default: 1]:[/{UIColors.HIGHLIGHT}]",
|
||||
valid_keys=["1", "2", "3"],
|
||||
default="1",
|
||||
error_message="Invalid input '{response}'. Please enter '1', '2', or '3'.",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _get_char_windows() -> str:
|
||||
import msvcrt
|
||||
@@ -267,13 +404,32 @@ class CharReader:
|
||||
for encoding in encodings:
|
||||
try:
|
||||
ch = msvcrt.getch()
|
||||
return ch.decode(encoding).lower()
|
||||
# Detect Ctrl+C (ETX character \x03)
|
||||
if isinstance(ch, bytes):
|
||||
if ch == b"\x03":
|
||||
raise KeyboardInterrupt
|
||||
decoded = ch.decode(encoding).lower()
|
||||
else:
|
||||
if ord(ch) == 3: # Ctrl+C
|
||||
raise KeyboardInterrupt
|
||||
decoded = ch.lower()
|
||||
return decoded
|
||||
except (UnicodeDecodeError, AttributeError):
|
||||
continue
|
||||
|
||||
# Fallback
|
||||
try:
|
||||
return msvcrt.getch().lower()
|
||||
ch = msvcrt.getch()
|
||||
if isinstance(ch, bytes):
|
||||
if ch == b"\x03":
|
||||
raise KeyboardInterrupt
|
||||
return ch.decode("utf-8", errors="ignore").lower()
|
||||
else:
|
||||
if ord(ch) == 3: # Ctrl+C
|
||||
raise KeyboardInterrupt
|
||||
return ch.lower()
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
@@ -284,70 +440,101 @@ class CharReader:
|
||||
try:
|
||||
tty.setraw(sys.stdin.fileno())
|
||||
ch = sys.stdin.read(1)
|
||||
# Detect Ctrl+C (ETX character \x03)
|
||||
if ord(ch) == 3: # Ctrl+C
|
||||
raise KeyboardInterrupt
|
||||
return ch.lower()
|
||||
finally:
|
||||
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
||||
|
||||
|
||||
def get_user_input() -> Tuple[str, str, str, list, dict]:
|
||||
collector = InputCollector()
|
||||
console.print()
|
||||
try:
|
||||
collector = InputCollector()
|
||||
console.print()
|
||||
|
||||
# Section 1: Project Setup
|
||||
UIFormatter.print_separator()
|
||||
console.print(f"\n[{UIColors.INFO}]Step 1: Project Setup[/{UIColors.INFO}]\n")
|
||||
# Pre-step: Ask for structure type first
|
||||
UIFormatter.print_separator()
|
||||
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()
|
||||
|
||||
console.print(f"[{UIColors.HIGHLIGHT}]Project Directory[/{UIColors.HIGHLIGHT}]")
|
||||
console.print(f"[{UIColors.MUTED}]Where your project files will be created[/{UIColors.MUTED}]")
|
||||
project_dir = collector.get_validated_input(
|
||||
"Enter project directory name", validate_project_name, "project directory name"
|
||||
)
|
||||
structure_choice = CharReader.get_structure_choice()
|
||||
|
||||
# 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"
|
||||
use_predefined = structure_choice == "2"
|
||||
use_unified = structure_choice == "3"
|
||||
|
||||
if use_predefined or use_unified:
|
||||
UIFormatter.print_separator()
|
||||
console.print(f"\n[{UIColors.INFO}]Step 1: Project Setup[/{UIColors.INFO}]\n")
|
||||
console.print(
|
||||
f"[{UIColors.MUTED}]Press Enter or enter '.' to create in current directory[/{UIColors.MUTED}]"
|
||||
)
|
||||
project_dir = collector._get_project_directory()
|
||||
if project_dir is None:
|
||||
UIFormatter.print_error("Maximum attempts reached for project directory name. Exiting.")
|
||||
sys.exit(1)
|
||||
|
||||
project_name, primary_app, app_names, metadata_dict = collector._get_structure_metadata(
|
||||
project_dir, predefined=use_predefined, unified=use_unified
|
||||
)
|
||||
|
||||
return project_dir, project_name, primary_app, app_names, metadata_dict
|
||||
|
||||
# Section 1: Project Setup (standard flow)
|
||||
UIFormatter.print_separator()
|
||||
console.print(f"\n[{UIColors.INFO}]Step 1: Project Setup[/{UIColors.INFO}]\n")
|
||||
console.print(f"[{UIColors.MUTED}]Press Enter or enter '.' to create in current directory[/{UIColors.MUTED}]")
|
||||
|
||||
project_dir = collector._get_project_directory()
|
||||
|
||||
if project_dir is None:
|
||||
UIFormatter.print_error("Maximum attempts reached for project directory name. Exiting.")
|
||||
sys.exit(1)
|
||||
|
||||
console.print()
|
||||
|
||||
console.print(f"[{UIColors.MUTED}]Common names: config, core, settings[/{UIColors.MUTED}]")
|
||||
project_name = collector.get_validated_input(
|
||||
"Enter Django project name", validate_project_name, "Django project name"
|
||||
)
|
||||
console.print()
|
||||
console.print()
|
||||
|
||||
console.print(f"[{UIColors.HIGHLIGHT}]Django Project Name[/{UIColors.HIGHLIGHT}]")
|
||||
console.print(f"[{UIColors.MUTED}]Name used in 'django-admin startproject' command[/{UIColors.MUTED}]")
|
||||
console.print(f"[{UIColors.MUTED}]Common names: config, core, settings, project_name[/{UIColors.MUTED}]")
|
||||
project_name = collector.get_validated_input(
|
||||
"Enter Django project name", validate_project_name, "Django project name"
|
||||
)
|
||||
console.print()
|
||||
# Section 2: Django Apps
|
||||
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()
|
||||
|
||||
# Section 2: Django Apps
|
||||
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()
|
||||
# Section 3: CI/CD Pipeline
|
||||
use_github, use_gitlab = collector.get_cicd_choice()
|
||||
|
||||
# Section 3: CI/CD Pipeline
|
||||
use_github, use_gitlab = collector.get_cicd_choice()
|
||||
# Section 4: Database Configuration
|
||||
use_database_url = collector.get_database_config_choice()
|
||||
|
||||
# Section 4: Database Configuration
|
||||
use_database_url = collector.get_database_config_choice()
|
||||
# Create metadata
|
||||
# Default package_name to "backend" if project_dir is "." or empty
|
||||
package_name = get_package_name(project_dir)
|
||||
metadata = ProjectMetadata(
|
||||
package_name=package_name,
|
||||
use_github_actions=use_github,
|
||||
use_gitlab_ci=use_gitlab,
|
||||
nested_apps=nested,
|
||||
nested_dir=nested_dir,
|
||||
use_database_url=use_database_url,
|
||||
)
|
||||
|
||||
# Create metadata
|
||||
metadata = ProjectMetadata(
|
||||
package_name=project_dir,
|
||||
use_github_actions=use_github,
|
||||
use_gitlab_ci=use_gitlab,
|
||||
nested_apps=nested,
|
||||
nested_dir=nested_dir,
|
||||
use_database_url=use_database_url,
|
||||
)
|
||||
console.print()
|
||||
|
||||
console.print()
|
||||
|
||||
# Return in legacy format for backward compatibility
|
||||
return project_dir, project_name, app_names[0], app_names, metadata.to_dict()
|
||||
# Return in legacy format for backward compatibility
|
||||
return project_dir, project_name, app_names[0], app_names, metadata.to_dict()
|
||||
except KeyboardInterrupt:
|
||||
UIFormatter.print_info("\nSetup cancelled by user.")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def confirm_setup(project_dir: str, project_name: str, app_names: list, metadata: dict) -> bool:
|
||||
@@ -357,17 +544,14 @@ def confirm_setup(project_dir: str, project_name: str, app_names: list, metadata
|
||||
console.print(f"[{UIColors.INFO}]Setup Summary[/{UIColors.INFO}]")
|
||||
console.print()
|
||||
|
||||
# Display configuration
|
||||
console.print(f"[{UIColors.HIGHLIGHT}]Project Directory:[/{UIColors.HIGHLIGHT}] {project_dir}")
|
||||
console.print(f"[{UIColors.HIGHLIGHT}]Django Project:[/{UIColors.HIGHLIGHT}] {project_name}")
|
||||
console.print(f"[{UIColors.HIGHLIGHT}]Apps:[/{UIColors.HIGHLIGHT}] {', '.join(app_names)}")
|
||||
console.print(f"[{UIColors.HIGHLIGHT}]Package:[/{UIColors.HIGHLIGHT}] {metadata['package_name']}")
|
||||
|
||||
# Show CI/CD choices
|
||||
cicd_choices = _get_cicd_display(metadata)
|
||||
console.print(f"[{UIColors.HIGHLIGHT}]CI/CD:[/{UIColors.HIGHLIGHT}] {cicd_choices}")
|
||||
|
||||
# Show database configuration
|
||||
db_config = "DATABASE_URL" if metadata.get("use_database_url", True) else "Individual parameters"
|
||||
console.print(f"[{UIColors.HIGHLIGHT}]Database Config:[/{UIColors.HIGHLIGHT}] {db_config}")
|
||||
|
||||
@@ -375,15 +559,8 @@ def confirm_setup(project_dir: str, project_name: str, app_names: list, metadata
|
||||
UIFormatter.print_separator()
|
||||
console.print()
|
||||
|
||||
# Get confirmation
|
||||
try:
|
||||
console.print(f"[{UIColors.WARNING}]Proceed with setup? (y/N):[/{UIColors.WARNING}] ", end="")
|
||||
sys.stdout.flush()
|
||||
|
||||
reader = CharReader()
|
||||
response = reader.get_char()
|
||||
console.print(response.upper())
|
||||
|
||||
response = CharReader.get_yes_no(f"[{UIColors.WARNING}]Proceed with setup? (y/N):[/{UIColors.WARNING}]")
|
||||
return response == "y"
|
||||
|
||||
except KeyboardInterrupt:
|
||||
@@ -392,14 +569,12 @@ def confirm_setup(project_dir: str, project_name: str, app_names: list, metadata
|
||||
|
||||
|
||||
def _get_cicd_display(metadata: dict) -> str:
|
||||
cicd_choices = []
|
||||
|
||||
choices = []
|
||||
if metadata.get("use_github_actions", False):
|
||||
cicd_choices.append("GitHub Actions")
|
||||
choices.append("GitHub Actions")
|
||||
if metadata.get("use_gitlab_ci", False):
|
||||
cicd_choices.append("GitLab CI")
|
||||
|
||||
return ", ".join(cicd_choices) if cicd_choices else "None"
|
||||
choices.append("GitLab CI")
|
||||
return ", ".join(choices) if choices else "None"
|
||||
|
||||
|
||||
get_char = CharReader.get_char
|
||||
|
||||
+102
-66
@@ -3,13 +3,18 @@ Project management for djinit.
|
||||
Handles creation of Django projects and apps.
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from djinit.scripts.console_ui import UIFormatter
|
||||
from djinit.utils import change_cwd
|
||||
from djinit.scripts.django_helper import DjangoHelper
|
||||
from djinit.utils import (
|
||||
calculate_app_module_paths,
|
||||
create_directory_with_init,
|
||||
extract_existing_apps,
|
||||
get_base_settings_path,
|
||||
insert_apps_into_user_defined_apps,
|
||||
read_base_settings,
|
||||
)
|
||||
|
||||
|
||||
class ProjectManager:
|
||||
@@ -18,88 +23,119 @@ class ProjectManager:
|
||||
self.project_name = project_name
|
||||
self.app_names = app_names
|
||||
self.metadata = metadata
|
||||
self.project_root = os.path.join(os.getcwd(), project_dir)
|
||||
self.project_root = os.getcwd() if project_dir == "." else os.path.join(os.getcwd(), project_dir)
|
||||
# Support custom module name for the Django config package (e.g., 'config')
|
||||
# Fall back to project_name if key present but None
|
||||
self.module_name = metadata.get("project_module_name") or self.project_name
|
||||
|
||||
def create_project(self) -> bool:
|
||||
self._ensure_django_installed()
|
||||
|
||||
# Create the project directory first
|
||||
os.makedirs(self.project_root, exist_ok=True)
|
||||
|
||||
# Create Django project inside the directory
|
||||
subprocess.run(
|
||||
["django-admin", "startproject", self.project_name, self.project_root],
|
||||
check=True,
|
||||
)
|
||||
unified = self.metadata.get("unified_structure", False)
|
||||
success = DjangoHelper.startproject(self.module_name, self.project_root, unified=unified)
|
||||
if success:
|
||||
UIFormatter.print_success(f"Django project '{self.project_name}' created successfully!")
|
||||
else:
|
||||
UIFormatter.print_error(f"Failed to create Django project '{self.project_name}'")
|
||||
return success
|
||||
|
||||
UIFormatter.print_success(f"Django project '{self.project_name}' created successfully!")
|
||||
return True
|
||||
def _get_apps_base_dir(self) -> str:
|
||||
"""Get the base directory for apps, considering nested structure."""
|
||||
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"))
|
||||
return apps_base_dir
|
||||
|
||||
def create_apps(self) -> bool:
|
||||
# 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
|
||||
apps_base_dir = self._get_apps_base_dir()
|
||||
|
||||
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()
|
||||
if apps_base_dir != self.project_root:
|
||||
create_directory_with_init(apps_base_dir, f"Created {os.path.basename(apps_base_dir)}/__init__.py")
|
||||
|
||||
with change_cwd(apps_base_dir):
|
||||
# Create each app
|
||||
for app_name in self.app_names:
|
||||
subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
os.path.join(self.project_root, "manage.py"),
|
||||
"startapp",
|
||||
app_name,
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
UIFormatter.print_success(f"Django app '{app_name}' created successfully!")
|
||||
for app_name in self.app_names:
|
||||
success = DjangoHelper.startapp(app_name, apps_base_dir)
|
||||
if not success:
|
||||
UIFormatter.print_error(f"Failed to create Django app '{app_name}'")
|
||||
return False
|
||||
UIFormatter.print_success(f"Django app '{app_name}' created successfully!")
|
||||
|
||||
# Add all apps to USER_DEFINED_APPS in settings
|
||||
if not self.add_apps_to_settings():
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _ensure_django_installed(self) -> None:
|
||||
if importlib.util.find_spec("django") is None:
|
||||
subprocess.run(
|
||||
[sys.executable, "-m", "pip", "install", "--upgrade", "django", "-q"],
|
||||
check=True,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
def add_apps_to_settings(self) -> bool:
|
||||
"""Add all apps to USER_DEFINED_APPS in base.py settings file."""
|
||||
base_settings_path = get_base_settings_path(self.project_root, self.module_name)
|
||||
|
||||
if not os.path.exists(base_settings_path):
|
||||
UIFormatter.print_error("Could not find base.py settings file")
|
||||
return False
|
||||
|
||||
content = read_base_settings(self.project_root, self.module_name)
|
||||
if content is None:
|
||||
UIFormatter.print_error("Could not read base.py settings file")
|
||||
return False
|
||||
|
||||
app_module_paths = calculate_app_module_paths(self.app_names, self.metadata)
|
||||
|
||||
existing_apps = extract_existing_apps(content)
|
||||
apps_to_add = [app for app in app_module_paths if app not in existing_apps]
|
||||
|
||||
if not apps_to_add:
|
||||
UIFormatter.print_success("All apps already configured in USER_DEFINED_APPS")
|
||||
return True
|
||||
|
||||
updated_content = insert_apps_into_user_defined_apps(content, apps_to_add)
|
||||
if not updated_content:
|
||||
return False
|
||||
|
||||
with open(base_settings_path, "w") as f:
|
||||
f.write(updated_content)
|
||||
|
||||
added_apps_str = ", ".join(apps_to_add)
|
||||
UIFormatter.print_success(f"Added apps to USER_DEFINED_APPS: {added_apps_str}")
|
||||
return True
|
||||
|
||||
def validate_project_structure(self) -> bool:
|
||||
required_files = [
|
||||
os.path.join(self.project_root, "manage.py"),
|
||||
os.path.join(self.project_root, self.project_name, "__init__.py"),
|
||||
os.path.join(self.project_root, self.project_name, "settings.py"),
|
||||
os.path.join(self.project_root, self.project_name, "urls.py"),
|
||||
os.path.join(self.project_root, self.project_name, "wsgi.py"),
|
||||
os.path.join(self.project_root, self.project_name, "asgi.py"),
|
||||
os.path.join(self.project_root, self.module_name, "__init__.py"),
|
||||
os.path.join(self.project_root, self.module_name, "settings", "__init__.py"),
|
||||
os.path.join(self.project_root, self.module_name, "settings", "base.py"),
|
||||
os.path.join(self.project_root, self.module_name, "settings", "development.py"),
|
||||
os.path.join(self.project_root, self.module_name, "settings", "production.py"),
|
||||
os.path.join(self.project_root, self.module_name, "urls.py"),
|
||||
os.path.join(self.project_root, self.module_name, "wsgi.py"),
|
||||
os.path.join(self.project_root, self.module_name, "asgi.py"),
|
||||
]
|
||||
|
||||
# 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"))
|
||||
apps_base_dir = self._get_apps_base_dir()
|
||||
|
||||
for app_name in self.app_names:
|
||||
app_files = [
|
||||
os.path.join(apps_base_dir, app_name, "__init__.py"),
|
||||
os.path.join(apps_base_dir, app_name, "apps.py"),
|
||||
os.path.join(apps_base_dir, app_name, "models.py"),
|
||||
os.path.join(apps_base_dir, app_name, "views.py"),
|
||||
os.path.join(apps_base_dir, app_name, "admin.py"),
|
||||
]
|
||||
required_files.extend(app_files)
|
||||
# In predefined or unified structure, apps layout differs; skip strict per-app validation
|
||||
if not self.metadata.get("predefined_structure") and not self.metadata.get("unified_structure"):
|
||||
for app_name in self.app_names:
|
||||
app_files = [
|
||||
os.path.join(apps_base_dir, app_name, "__init__.py"),
|
||||
os.path.join(apps_base_dir, app_name, "apps.py"),
|
||||
os.path.join(apps_base_dir, app_name, "models.py"),
|
||||
os.path.join(apps_base_dir, app_name, "views.py"),
|
||||
os.path.join(apps_base_dir, app_name, "serializers.py"),
|
||||
os.path.join(apps_base_dir, app_name, "routes.py"),
|
||||
os.path.join(apps_base_dir, app_name, "tests.py"),
|
||||
os.path.join(apps_base_dir, app_name, "migrations"),
|
||||
os.path.join(apps_base_dir, app_name, "admin.py"),
|
||||
]
|
||||
required_files.extend(app_files)
|
||||
|
||||
missing_files = [file_path for file_path in required_files if not os.path.exists(file_path)]
|
||||
missing_files = []
|
||||
for file_path in required_files:
|
||||
if file_path.endswith("settings"):
|
||||
if not os.path.isdir(file_path):
|
||||
missing_files.append(file_path)
|
||||
elif not os.path.exists(file_path):
|
||||
missing_files.append(file_path)
|
||||
|
||||
if not missing_files:
|
||||
UIFormatter.print_success("Project structure validation passed")
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
"""
|
||||
Settings management for djinit.
|
||||
Handles creation and configuration of Django settings files.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from djinit.scripts.console_ui import UIFormatter
|
||||
from djinit.scripts.secretkey_generator import generate_secret_key
|
||||
from djinit.scripts.template_engine import template_engine
|
||||
from djinit.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)
|
||||
create_file_with_content(
|
||||
"base.py",
|
||||
base_content,
|
||||
"Updated settings/base.py with comprehensive configuration",
|
||||
should_format=True,
|
||||
)
|
||||
return 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)
|
||||
create_file_with_content(
|
||||
"development.py",
|
||||
dev_content,
|
||||
"Updated settings/development.py",
|
||||
should_format=True,
|
||||
)
|
||||
return True
|
||||
|
||||
def update_production_settings(self) -> bool:
|
||||
"""Update production.py with production-specific settings."""
|
||||
with change_cwd(self.settings_folder):
|
||||
context = {"use_database_url": self.metadata.get("use_database_url", True)}
|
||||
prod_content = template_engine.render_template("settings_production.j2", context)
|
||||
create_file_with_content(
|
||||
"production.py",
|
||||
prod_content,
|
||||
"Updated settings/production.py",
|
||||
should_format=True,
|
||||
)
|
||||
return True
|
||||
@@ -76,5 +76,4 @@ class TemplateEngine:
|
||||
return os.path.exists(template_path)
|
||||
|
||||
|
||||
# Global template engine instance
|
||||
template_engine = TemplateEngine()
|
||||
@@ -0,0 +1,5 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
@@ -0,0 +1,8 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class {{ app_name|title }}Config(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = '{{ app_name }}'
|
||||
Whitespace-only changes.
@@ -0,0 +1,5 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
@@ -1,4 +1,6 @@
|
||||
"""
|
||||
generated with djinit
|
||||
|
||||
API routes for {{ app_name }} app.
|
||||
"""
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""
|
||||
Serializers for {{ app_name }} app.
|
||||
generated with djinit
|
||||
serializers for {{ app_name }} app.
|
||||
"""
|
||||
|
||||
from rest_framework import serializers
|
||||
@@ -0,0 +1,5 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@@ -1,3 +1,5 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
@@ -0,0 +1,7 @@
|
||||
from django.urls import include, path
|
||||
|
||||
app_name = "api"
|
||||
|
||||
urlpatterns = [
|
||||
path("v1/", include("api.v1.urls")),
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
from django.urls import include, path
|
||||
|
||||
app_name = "v1"
|
||||
|
||||
urlpatterns = [
|
||||
path("users/", include("apps.users.urls")),
|
||||
]
|
||||
@@ -0,0 +1,8 @@
|
||||
from django.db import models
|
||||
|
||||
|
||||
class {{ model_class_name }}(models.Model):
|
||||
"""Generated model for {{ app_name }} app."""
|
||||
|
||||
# Add fields here
|
||||
pass
|
||||
@@ -0,0 +1,9 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from {{ app_module }}.models.{{ app_name }} import {{ model_class_name }}
|
||||
|
||||
|
||||
class {{ model_class_name }}Serializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = {{ model_class_name }}
|
||||
fields = "__all__"
|
||||
@@ -0,0 +1,10 @@
|
||||
from typing import Optional
|
||||
|
||||
from {{ app_module }}.models.{{ app_name }} import {{ model_class_name }}
|
||||
|
||||
|
||||
def get_{{ app_name }}_by_id(object_id: int) -> Optional[{{ model_class_name }}]:
|
||||
try:
|
||||
return {{ model_class_name }}.objects.get(id=object_id)
|
||||
except {{ model_class_name }}.DoesNotExist:
|
||||
return None
|
||||
@@ -0,0 +1,12 @@
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_{{ app_name }}_list_empty():
|
||||
client = APIClient()
|
||||
url = reverse("v1:{{ app_name }}-list")
|
||||
response = client.get(url)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == []
|
||||
@@ -0,0 +1,14 @@
|
||||
from django.urls import include, path
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
from {{ app_module }}.views.{{ app_name }}_view import {{ model_class_name }}ViewSet
|
||||
|
||||
|
||||
app_name = "{{ app_name }}"
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r"{{ app_name }}", {{ model_class_name }}ViewSet, basename="{{ app_name }}")
|
||||
|
||||
urlpatterns = [
|
||||
path("", include(router.urls)),
|
||||
]
|
||||
@@ -0,0 +1,9 @@
|
||||
from rest_framework import viewsets
|
||||
|
||||
from {{ app_module }}.models.{{ app_name }} import {{ model_class_name }}
|
||||
from {{ app_module }}.serializers.{{ app_name }}_serializer import {{ model_class_name }}Serializer
|
||||
|
||||
|
||||
class {{ model_class_name }}ViewSet(viewsets.ModelViewSet):
|
||||
queryset = {{ model_class_name }}.objects.all()
|
||||
serializer_class = {{ model_class_name }}Serializer
|
||||
@@ -0,0 +1,9 @@
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.db import models
|
||||
|
||||
|
||||
class User(AbstractUser):
|
||||
"""Custom user model extending Django's AbstractUser."""
|
||||
|
||||
# Add custom fields here if needed
|
||||
pass
|
||||
@@ -0,0 +1,9 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from apps.users.models.user import User
|
||||
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ["id", "username", "email", "first_name", "last_name"]
|
||||
@@ -0,0 +1,10 @@
|
||||
from typing import Optional
|
||||
|
||||
from apps.users.models.user import User
|
||||
|
||||
|
||||
def get_user_by_id(user_id: int) -> Optional[User]:
|
||||
try:
|
||||
return User.objects.get(id=user_id)
|
||||
except User.DoesNotExist:
|
||||
return None
|
||||
@@ -0,0 +1,12 @@
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_users_list_empty():
|
||||
client = APIClient()
|
||||
url = reverse("v1:users-list")
|
||||
response = client.get(url)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == []
|
||||
@@ -0,0 +1,14 @@
|
||||
from django.urls import include, path
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
from apps.users.views.user_view import UserViewSet
|
||||
|
||||
|
||||
app_name = "users"
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r"users", UserViewSet, basename="users")
|
||||
|
||||
urlpatterns = [
|
||||
path("", include(router.urls)),
|
||||
]
|
||||
@@ -0,0 +1,13 @@
|
||||
from rest_framework import status, viewsets
|
||||
from rest_framework.response import Response
|
||||
|
||||
from apps.users.models.user import User
|
||||
from apps.users.serializers.user_serializer import UserSerializer
|
||||
|
||||
|
||||
class UserViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
queryset = User.objects.all()
|
||||
serializer_class = UserSerializer
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
return super().list(request, *args, **kwargs)
|
||||
@@ -0,0 +1,5 @@
|
||||
class ApplicationError(Exception):
|
||||
"""Base application exception."""
|
||||
|
||||
def __init__(self, message: str):
|
||||
super().__init__(message)
|
||||
@@ -0,0 +1,14 @@
|
||||
import logging
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RequestLoggerMiddleware:
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
logger.info("%s %s", request.method, request.path)
|
||||
response = self.get_response(request)
|
||||
return response
|
||||
@@ -0,0 +1,9 @@
|
||||
from django.db import models
|
||||
|
||||
|
||||
class TimestampedModel(models.Model):
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
@@ -0,0 +1,9 @@
|
||||
from rest_framework.response import Response
|
||||
|
||||
|
||||
def success(data=None, status_code=200):
|
||||
return Response({"success": True, "data": data}, status=status_code)
|
||||
|
||||
|
||||
def error(message, status_code=400):
|
||||
return Response({"success": False, "message": message}, status=status_code)
|
||||
@@ -1,9 +1,10 @@
|
||||
"""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"))
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", os.getenv("DJANGO_SETTINGS_MODULE", "core.settings.production"))
|
||||
application = get_asgi_application()
|
||||
@@ -1,4 +1,5 @@
|
||||
"""
|
||||
generated with djinit
|
||||
Common settings shared between development and production environment
|
||||
"""
|
||||
|
||||
@@ -148,13 +149,28 @@ SIMPLE_JWT = {
|
||||
}
|
||||
|
||||
# CORS Settings
|
||||
CORS_ALLOWED_ORIGINS = [
|
||||
"http://localhost:3000",
|
||||
"http://127.0.0.1:3000",
|
||||
CORS_ALLOW_METHODS = [
|
||||
"DELETE",
|
||||
"GET",
|
||||
"OPTIONS",
|
||||
"PATCH",
|
||||
"POST",
|
||||
"PUT",
|
||||
]
|
||||
|
||||
CORS_ALLOW_CREDENTIALS = True
|
||||
CORS_ALLOW_HEADERS = [
|
||||
"accept",
|
||||
"accept-encoding",
|
||||
"authorization",
|
||||
"content-type",
|
||||
"dnt",
|
||||
"origin",
|
||||
"user-agent",
|
||||
"x-csrftoken",
|
||||
"x-requested-with",
|
||||
]
|
||||
|
||||
CORS_PREFLIGHT_MAX_AGE = 86400 # 24 hours
|
||||
# Security Settings
|
||||
SECURE_BROWSER_XSS_FILTER = True
|
||||
SECURE_CONTENT_TYPE_NOSNIFF = True
|
||||
+4
@@ -1,3 +1,5 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from .base import *
|
||||
|
||||
SECRET_KEY = "{{ secret_key }}"
|
||||
@@ -21,7 +23,9 @@ DATABASES = {
|
||||
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
||||
|
||||
# CORS settings for development
|
||||
# Allow all origins in development (convenient but not secure)
|
||||
CORS_ALLOW_ALL_ORIGINS = True
|
||||
CORS_ALLOW_CREDENTIALS = True
|
||||
|
||||
# Disable HTTPS redirect in development
|
||||
SECURE_SSL_REDIRECT = False
|
||||
+13
@@ -1,3 +1,5 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from .base import *
|
||||
import os
|
||||
import dj_database_url
|
||||
@@ -26,7 +28,16 @@ DATABASES = {
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
# CORS settings for production
|
||||
# Filter out empty strings from FRONTEND_URL
|
||||
frontend_urls = os.getenv("FRONTEND_URL", "")
|
||||
CORS_ALLOWED_ORIGINS = [
|
||||
origin.strip() for origin in frontend_urls.split(",") if origin.strip()
|
||||
]
|
||||
CORS_ALLOW_CREDENTIALS = True
|
||||
|
||||
# Additional CORS security for production
|
||||
CORS_ALLOW_PRIVATE_NETWORK = False
|
||||
|
||||
# Email settings
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||
@@ -35,6 +46,8 @@ EMAIL_PORT = int(os.getenv('EMAIL_PORT', 587))
|
||||
EMAIL_USE_TLS = os.getenv('EMAIL_USE_TLS', 'True').lower() == 'true'
|
||||
EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER')
|
||||
EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD')
|
||||
DEFAULT_FROM_EMAIL = os.environ.get("EMAIL_HOST_USER")
|
||||
SERVER_EMAIL = os.environ.get("EMAIL_HOST_USER")
|
||||
|
||||
# Security settings for production
|
||||
SECURE_SSL_REDIRECT = True
|
||||
@@ -0,0 +1,8 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.contrib import admin
|
||||
from django.urls import path
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
]
|
||||
@@ -0,0 +1,29 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from django.conf import settings
|
||||
from drf_spectacular.views import (
|
||||
SpectacularAPIView,
|
||||
SpectacularSwaggerView,
|
||||
)
|
||||
from rest_framework_simplejwt.views import (
|
||||
TokenObtainPairView,
|
||||
TokenRefreshView,
|
||||
TokenBlacklistView,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
# JWT tokens
|
||||
path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
|
||||
path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
|
||||
path("token/blacklist/", TokenBlacklistView.as_view(), name="token_blacklist"),
|
||||
path("api/", include("{{ api_module }}.urls")),]
|
||||
|
||||
# Don't show schema in production
|
||||
if settings.DEBUG:
|
||||
urlpatterns += [
|
||||
path("schema/", SpectacularAPIView.as_view(), name="schema"),
|
||||
path("docs/", SpectacularSwaggerView.as_view(url_name="schema")),
|
||||
]
|
||||
@@ -1,3 +1,5 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from django.conf import settings
|
||||
@@ -1,9 +1,10 @@
|
||||
"""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"))
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", os.getenv("DJANGO_SETTINGS_MODULE", "core.settings.production"))
|
||||
|
||||
application = get_wsgi_application()
|
||||
@@ -1,60 +0,0 @@
|
||||
# {{ 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/
|
||||
+4
-4
@@ -31,10 +31,10 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
{% raw %}- name: Set up Python ${{ matrix.python-version }}{% endraw %}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
{% raw %}python-version: ${{ matrix.python-version }}{% endraw %}
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
|
||||
- name: Run Django tests
|
||||
env:
|
||||
DJANGO_SETTINGS_MODULE: {{ project_name }}.settings.development
|
||||
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
DB_NAME: test_db
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
|
||||
- name: Check migrations
|
||||
env:
|
||||
DJANGO_SETTINGS_MODULE: {{ project_name }}.settings.development
|
||||
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
DB_NAME: test_db
|
||||
@@ -36,7 +36,7 @@ test:
|
||||
stage: test
|
||||
image: python:3.13
|
||||
variables:
|
||||
DJANGO_SETTINGS_MODULE: {{ project_name }}.settings.development
|
||||
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
script:
|
||||
@@ -52,7 +52,7 @@ test:python310:
|
||||
stage: test
|
||||
image: python:3.10
|
||||
variables:
|
||||
DJANGO_SETTINGS_MODULE: {{ project_name }}.settings.development
|
||||
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
script:
|
||||
@@ -69,7 +69,7 @@ test:python311:
|
||||
stage: test
|
||||
image: python:3.11
|
||||
variables:
|
||||
DJANGO_SETTINGS_MODULE: {{ project_name }}.settings.development
|
||||
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
script:
|
||||
@@ -86,7 +86,7 @@ test:python312:
|
||||
stage: test
|
||||
image: python:3.12
|
||||
variables:
|
||||
DJANGO_SETTINGS_MODULE: {{ project_name }}.settings.development
|
||||
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
script:
|
||||
@@ -103,7 +103,7 @@ test:python313:
|
||||
stage: test
|
||||
image: python:3.13
|
||||
variables:
|
||||
DJANGO_SETTINGS_MODULE: {{ project_name }}.settings.development
|
||||
DJANGO_SETTINGS_MODULE: {{ module_name }}.settings.development
|
||||
SECRET_KEY: test-secret-key-for-ci
|
||||
ALLOWED_HOSTS: localhost
|
||||
script:
|
||||
@@ -15,9 +15,14 @@ DB_HOST=
|
||||
DB_PORT=
|
||||
{% endif %}
|
||||
|
||||
# Frontend URL
|
||||
FRONTEND_URL=http://frontend.your-domain.com
|
||||
|
||||
# Email settings
|
||||
EMAIL_HOST=
|
||||
EMAIL_PORT=
|
||||
EMAIL_USE_TLS=
|
||||
EMAIL_HOST_USER=
|
||||
EMAIL_HOST_PASSWORD=
|
||||
DEFAULT_FROM_EMAIL=
|
||||
SERVER_EMAIL=
|
||||
@@ -1,3 +1,7 @@
|
||||
#--------------------------------------------------#
|
||||
# The following was generated by djinit: #
|
||||
#--------------------------------------------------#
|
||||
|
||||
# Python
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
Whitespace-only changes.
@@ -12,7 +12,7 @@ dev:
|
||||
|
||||
# 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}}
|
||||
uv run python manage.py runserver {{ '{{' }}HOST{{ '}}' }}:{{ '{{' }}PORT{{ '}}' }}
|
||||
|
||||
# Create migrations for all apps
|
||||
makemigrations:
|
||||
@@ -20,7 +20,7 @@ makemigrations:
|
||||
|
||||
# Create migrations for specific app
|
||||
makemigrations-app APP:
|
||||
uv run python manage.py makemigrations {{APP}}
|
||||
uv run python manage.py makemigrations {{ '{{' }}APP{{ '}}' }}
|
||||
|
||||
# Run all migrations
|
||||
migrate:
|
||||
@@ -112,7 +112,7 @@ server:
|
||||
|
||||
# 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}}
|
||||
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:
|
||||
@@ -143,12 +143,12 @@ setup-complete:
|
||||
|
||||
# Install a new package
|
||||
install PACKAGE:
|
||||
uv add {{PACKAGE}}
|
||||
uv add {{ '{{' }}PACKAGE{{ '}}' }}
|
||||
|
||||
# Install a dev package
|
||||
install-dev PACKAGE:
|
||||
uv add --dev {{PACKAGE}}
|
||||
uv add --dev {{ '{{' }}PACKAGE{{ '}}' }}
|
||||
|
||||
# Remove a package
|
||||
remove PACKAGE:
|
||||
uv remove {{PACKAGE}}
|
||||
uv remove {{ '{{' }}PACKAGE{{ '}}' }}
|
||||
@@ -1,7 +1,11 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""generated with djinit"""
|
||||
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
|
||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,148 @@
|
||||
# {{ 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
|
||||
|
||||
```
|
||||
{% if predefined_structure %}
|
||||
{{ project_name }}/
|
||||
├── {{ module_name }}/
|
||||
│ ├── settings/
|
||||
│ │ ├── base.py
|
||||
│ │ ├── development.py
|
||||
│ │ └── production.py
|
||||
│ ├── urls.py
|
||||
│ ├── wsgi.py
|
||||
│ └── asgi.py
|
||||
├── apps/
|
||||
│ ├── users/
|
||||
│ │ ├── apps.py
|
||||
│ │ ├── models/
|
||||
│ │ │ └── user.py
|
||||
│ │ ├── serializers/
|
||||
│ │ │ └── user_serializer.py
|
||||
│ │ ├── services/
|
||||
│ │ │ └── user_service.py
|
||||
│ │ ├── views/
|
||||
│ │ │ └── user_view.py
|
||||
│ │ ├── urls.py
|
||||
│ │ └── tests/
|
||||
│ │ └── test_user_api.py
|
||||
│ └── core/
|
||||
│ ├── utils/
|
||||
│ │ └── responses.py
|
||||
│ ├── mixins/
|
||||
│ │ └── timestamped_model.py
|
||||
│ ├── exceptions.py
|
||||
│ └── middleware/
|
||||
│ └── request_logger.py
|
||||
├── api/
|
||||
│ ├── __init__.py
|
||||
│ ├── urls.py
|
||||
│ └── v1/
|
||||
│ ├── __init__.py
|
||||
│ └── urls.py
|
||||
├── manage.py
|
||||
├── requirements.txt
|
||||
├── pyproject.toml
|
||||
├── .env.sample
|
||||
├── justfile
|
||||
├── Procfile
|
||||
├── runtime.txt
|
||||
└── README.md
|
||||
{% elif unified_structure %}
|
||||
{{ project_name }}/
|
||||
├── core/
|
||||
│ ├── settings/
|
||||
│ │ ├── base.py
|
||||
│ │ ├── development.py
|
||||
│ │ └── production.py
|
||||
│ ├── urls.py
|
||||
│ ├── wsgi.py
|
||||
│ └── asgi.py
|
||||
├── apps/
|
||||
│ ├── core/
|
||||
│ │ ├── apps.py
|
||||
│ │ ├── models/
|
||||
│ │ │ ├── __init__.py
|
||||
│ │ │ ├── base.py
|
||||
│ │ │ └── mixins.py
|
||||
│ │ ├── utils/
|
||||
│ │ │ ├── __init__.py
|
||||
│ │ │ └── responses.py
|
||||
│ │ ├── permissions/
|
||||
│ │ ├── middleware/
|
||||
│ │ └── __init__.py
|
||||
│ └── api/
|
||||
│ ├── apps.py
|
||||
│ ├── models/
|
||||
│ ├── serializers/
|
||||
│ ├── views/
|
||||
│ ├── services/
|
||||
│ ├── tests/
|
||||
│ ├── urls/
|
||||
│ └── admin/
|
||||
├── manage.py
|
||||
├── requirements.txt
|
||||
├── pyproject.toml
|
||||
├── .env.sample
|
||||
├── justfile
|
||||
├── Procfile
|
||||
├── runtime.txt
|
||||
└── README.md
|
||||
{% else %}
|
||||
{{ project_name }}/
|
||||
├── {{ project_name }}/
|
||||
│ ├── 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
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
## API Documentation
|
||||
|
||||
When running in development mode, API documentation is available at:
|
||||
- Swagger UI: http://localhost:8000/docs/
|
||||
- Schema: http://localhost:8000/schema/
|
||||
@@ -1,6 +1,10 @@
|
||||
#--------------------------------------------------#
|
||||
# The following was generated by djinit: #
|
||||
#--------------------------------------------------#
|
||||
|
||||
Django
|
||||
python-dotenv
|
||||
django-jazzmin>
|
||||
django-jazzmin
|
||||
djangorestframework
|
||||
djangorestframework_simplejwt
|
||||
drf-spectacular
|
||||
File renamed without changes.
@@ -0,0 +1,8 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CoreConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.core"
|
||||
@@ -0,0 +1,4 @@
|
||||
from .base import AbstractBaseModel
|
||||
from .mixins import TimestampedModel
|
||||
|
||||
__all__ = ["AbstractBaseModel", "TimestampedModel"]
|
||||
@@ -0,0 +1,13 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.db import models
|
||||
|
||||
|
||||
class AbstractBaseModel(models.Model):
|
||||
"""Abstract base model with common fields."""
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
@@ -0,0 +1,13 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.db import models
|
||||
|
||||
|
||||
class TimestampedModel(models.Model):
|
||||
"""Mixin for models that need timestamp fields."""
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
@@ -0,0 +1,3 @@
|
||||
from .responses import success, error
|
||||
|
||||
__all__ = ["success", "error"]
|
||||
@@ -0,0 +1,13 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from rest_framework.response import Response
|
||||
|
||||
|
||||
def success(data=None, status_code=200):
|
||||
"""Return a successful response."""
|
||||
return Response({"success": True, "data": data}, status=status_code)
|
||||
|
||||
|
||||
def error(message, status_code=400):
|
||||
"""Return an error response."""
|
||||
return Response({"success": False, "message": message}, status=status_code)
|
||||
+258
-18
@@ -5,48 +5,288 @@ Contains common operations used across multiple managers.
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from contextlib import contextmanager
|
||||
|
||||
from djinit.scripts.console_ui import UIFormatter
|
||||
from djinit.scripts.template_engine import template_engine
|
||||
|
||||
|
||||
def format_file(filename: str) -> None:
|
||||
"""Format Python file using Ruff formatter."""
|
||||
subprocess.run(["ruff", "format", filename], check=False, capture_output=True)
|
||||
subprocess.run([sys.executable, "-m", "ruff", "format", filename], check=False, capture_output=True)
|
||||
|
||||
|
||||
# create a file with content, optionally format it
|
||||
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.
|
||||
|
||||
Args:
|
||||
filename: Name of the file to create
|
||||
content: Content to write to the file
|
||||
success_message: Message to print on success
|
||||
should_format: Whether to format the file with ruff
|
||||
|
||||
Returns:
|
||||
True if successful
|
||||
"""
|
||||
with open(filename, "w") as file:
|
||||
file.write(content)
|
||||
|
||||
if should_format:
|
||||
format_file(filename)
|
||||
|
||||
UIFormatter.print_success(success_message)
|
||||
# UIFormatter.print_success(success_message)
|
||||
return True
|
||||
|
||||
|
||||
# create a file from a template
|
||||
def create_file_from_template(
|
||||
file_path: str,
|
||||
template_path: str,
|
||||
context: dict,
|
||||
success_message: str,
|
||||
should_format: bool = True,
|
||||
) -> None:
|
||||
content = template_engine.render_template(template_path, context)
|
||||
create_file_with_content(file_path, content, success_message, should_format=should_format)
|
||||
|
||||
|
||||
# create __init__.py
|
||||
def create_init_file(directory: str, success_message: str) -> None:
|
||||
init_path = os.path.join(directory, "__init__.py")
|
||||
create_file_from_template(init_path, "shared/init.j2", {}, success_message, should_format=False)
|
||||
|
||||
|
||||
def create_directory_with_init(dir_path: str, message: str) -> None:
|
||||
"""Create directory and __init__.py file."""
|
||||
os.makedirs(dir_path, exist_ok=True)
|
||||
create_init_file(dir_path, message)
|
||||
|
||||
|
||||
def create_files_from_templates(base_dir: str, files: list, prefix: str = "", should_format: bool = False) -> None:
|
||||
"""Create multiple files from templates.
|
||||
|
||||
Args:
|
||||
base_dir: Base directory where files will be created
|
||||
files: List of tuples (filename, template_path, context)
|
||||
prefix: Optional prefix for success message
|
||||
should_format: Whether to format the created files
|
||||
"""
|
||||
for file_spec in files:
|
||||
if len(file_spec) == 3:
|
||||
filename, template_path, context = file_spec
|
||||
else:
|
||||
# Support (filename, template_path) format with empty context
|
||||
filename, template_path = file_spec
|
||||
context = {}
|
||||
filepath = os.path.join(base_dir, filename)
|
||||
message = f"Created {prefix}{filename}" if prefix else f"Created {filename}"
|
||||
create_file_from_template(filepath, template_path, context, message, should_format=should_format)
|
||||
|
||||
|
||||
def get_package_name(project_dir: str) -> str:
|
||||
"""Get package name, defaulting to 'backend' if project_dir is '.' or empty."""
|
||||
return "backend" if project_dir == "." or not project_dir else project_dir
|
||||
|
||||
|
||||
# change the current working dir tempporary
|
||||
@contextmanager
|
||||
def change_cwd(path: str):
|
||||
"""Temporarily change the current working directory.
|
||||
|
||||
Ensures the original directory is restored even if an exception occurs.
|
||||
"""
|
||||
original_cwd = os.getcwd()
|
||||
os.chdir(path)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
os.chdir(original_cwd)
|
||||
|
||||
|
||||
def _parse_app_entry_from_line(line: str) -> str | None:
|
||||
"""Extract app name from a line in USER_DEFINED_APPS section."""
|
||||
line_stripped = line.strip().strip(",").strip()
|
||||
for quote in ('"', "'"):
|
||||
if line_stripped.startswith(quote) and line_stripped.endswith(quote):
|
||||
return line_stripped.strip(quote)
|
||||
return None
|
||||
|
||||
|
||||
def _is_user_apps_section_start(line: str) -> bool:
|
||||
"""Check if line starts USER_DEFINED_APPS section."""
|
||||
return "USER_DEFINED_APPS" in line and "=" in line
|
||||
|
||||
|
||||
def _iterate_user_apps_lines(content: str):
|
||||
"""Generator that yields lines within USER_DEFINED_APPS section."""
|
||||
lines = content.split("\n")
|
||||
in_user_apps = False
|
||||
|
||||
for line in lines:
|
||||
if _is_user_apps_section_start(line):
|
||||
in_user_apps = True
|
||||
elif in_user_apps and line.strip() == "]":
|
||||
break
|
||||
elif in_user_apps:
|
||||
yield line
|
||||
|
||||
|
||||
# get app names from USER_DEFINED_APPS section in project_name/settings/base.py
|
||||
def extract_existing_apps(content: str) -> set:
|
||||
existing_apps = set()
|
||||
for line in _iterate_user_apps_lines(content):
|
||||
app_name = _parse_app_entry_from_line(line)
|
||||
if app_name:
|
||||
existing_apps.add(app_name)
|
||||
return existing_apps
|
||||
|
||||
|
||||
# format app names as list entries with proper indentation
|
||||
def format_app_entries(apps: list) -> list:
|
||||
return [f' "{app}",' for app in apps]
|
||||
|
||||
|
||||
# check if line starts a new APP section
|
||||
def is_app_section_start(line: str) -> bool:
|
||||
return "=" in line and any(keyword in line for keyword in ("BUILT_IN_APPS", "THIRD_PARTY_APPS", "INSTALLED_APPS"))
|
||||
|
||||
|
||||
# check if closing bracket belongs to USER_DEFINED_APPS by looking ahead
|
||||
def is_user_apps_closing_bracket(lines: list, current_idx: int) -> bool:
|
||||
for next_idx in range(current_idx + 1, min(current_idx + 3, len(lines))):
|
||||
next_line = lines[next_idx].strip()
|
||||
if next_line and "BUILT_IN_APPS" in next_line:
|
||||
return True
|
||||
return True # Default to True if we're inside USER_DEFINED_APPS
|
||||
|
||||
|
||||
# split line with closing bracnket and insert apps
|
||||
def split_bracket_line(line: str, apps_to_add: list) -> list:
|
||||
bracket_pos = line.rfind("]")
|
||||
before_bracket = line[:bracket_pos].rstrip() if bracket_pos > 0 else line.rstrip()
|
||||
after_bracket = line[bracket_pos:] if bracket_pos > 0 else "]"
|
||||
|
||||
result = [before_bracket]
|
||||
result.extend(format_app_entries(apps_to_add))
|
||||
result.append(after_bracket)
|
||||
return result
|
||||
|
||||
|
||||
# insert apps into USER_DEFINED_APPS section before the closing bracket.
|
||||
def insert_apps_into_user_defined_apps(content: str, apps_to_add: list) -> str:
|
||||
lines = content.split("\n")
|
||||
new_lines = []
|
||||
in_user_apps = False
|
||||
apps_added = False
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
# Entering USER_DEFINED_APPS section
|
||||
if _is_user_apps_section_start(line) and not apps_added:
|
||||
in_user_apps = True
|
||||
|
||||
# Handle closing bracket on same line
|
||||
if "]" in line:
|
||||
new_lines.extend(split_bracket_line(line, apps_to_add))
|
||||
apps_added = True
|
||||
in_user_apps = False
|
||||
else:
|
||||
new_lines.append(line)
|
||||
|
||||
# Closing bracket on separate line
|
||||
elif in_user_apps and line.strip() == "]" and not apps_added:
|
||||
if is_user_apps_closing_bracket(lines, i):
|
||||
new_lines.extend(format_app_entries(apps_to_add))
|
||||
new_lines.append(line)
|
||||
apps_added = True
|
||||
in_user_apps = False
|
||||
else:
|
||||
new_lines.append(line)
|
||||
|
||||
# Safety: hit another APP section without closing bracket
|
||||
elif in_user_apps and is_app_section_start(line) and not apps_added:
|
||||
new_lines.extend(format_app_entries(apps_to_add))
|
||||
new_lines.append("]")
|
||||
new_lines.append(line)
|
||||
apps_added = True
|
||||
in_user_apps = False
|
||||
|
||||
else:
|
||||
new_lines.append(line)
|
||||
|
||||
if not apps_added:
|
||||
UIFormatter.print_error("Could not find or update USER_DEFINED_APPS section in base.py")
|
||||
return None
|
||||
|
||||
return "\n".join(new_lines)
|
||||
|
||||
|
||||
# calculate app module paths based on nested structure
|
||||
def calculate_app_module_paths(app_names: list, metadata: dict) -> list:
|
||||
nested = bool(metadata.get("nested_apps"))
|
||||
nested_dir_name = metadata.get("nested_dir") if nested else None
|
||||
return [calculate_app_module_path(app_name, nested, nested_dir_name) for app_name in app_names]
|
||||
|
||||
|
||||
# calculate app module path for a single app
|
||||
def calculate_app_module_path(app_name: str, nested: bool, nested_dir: str | None) -> str:
|
||||
return f"{nested_dir}.{app_name}" if nested and nested_dir else app_name
|
||||
|
||||
|
||||
# check if a dir is a Django project by looking for manage.py
|
||||
def is_django_project(directory: str = None) -> bool:
|
||||
if directory is None:
|
||||
directory = os.getcwd()
|
||||
manage_py_path = os.path.join(directory, "manage.py")
|
||||
return os.path.exists(manage_py_path)
|
||||
|
||||
|
||||
# search the project dir that contains settings/base.py
|
||||
def find_project_dir(search_dir: str = None) -> tuple[str | None, str | None]:
|
||||
if search_dir is None:
|
||||
search_dir = os.getcwd()
|
||||
|
||||
for item in os.listdir(search_dir):
|
||||
if os.path.isdir(item) and not item.startswith(".") and item != "__pycache__":
|
||||
candidate = os.path.join(search_dir, item)
|
||||
base_py = os.path.join(candidate, "settings", "base.py")
|
||||
if os.path.exists(base_py):
|
||||
return candidate, base_py
|
||||
return None, None
|
||||
|
||||
|
||||
# search the project_name/settings dir path
|
||||
def find_settings_path(search_dir: str = None) -> str | None:
|
||||
project_dir, settings_base_path = find_project_dir(search_dir)
|
||||
if settings_base_path:
|
||||
return os.path.dirname(settings_base_path)
|
||||
return None
|
||||
|
||||
|
||||
# get the path to project_name/settings/base.py
|
||||
def get_base_settings_path(project_root: str, project_name: str) -> str:
|
||||
settings_dir = os.path.join(project_root, project_name, "settings")
|
||||
return os.path.join(settings_dir, "base.py")
|
||||
|
||||
|
||||
# read the content of project_name/settings/base.py
|
||||
def read_base_settings(project_root: str, project_name: str) -> str | None:
|
||||
base_settings_path = get_base_settings_path(project_root, project_name)
|
||||
if not os.path.exists(base_settings_path):
|
||||
return None
|
||||
|
||||
with open(base_settings_path) as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
# check for nested app structure(e.g: "apps.users" means nested) from project_name/settings/base.py
|
||||
def detect_nested_structure_from_settings(
|
||||
base_settings_path: str, search_dir: str = None
|
||||
) -> tuple[bool, str | None, str]:
|
||||
if search_dir is None:
|
||||
search_dir = os.getcwd()
|
||||
|
||||
if not os.path.exists(base_settings_path):
|
||||
return False, None, search_dir
|
||||
|
||||
try:
|
||||
with open(base_settings_path) as f:
|
||||
base_content = f.read()
|
||||
|
||||
for line in _iterate_user_apps_lines(base_content):
|
||||
app_label = _parse_app_entry_from_line(line)
|
||||
if app_label and "." in app_label:
|
||||
nested_dir_name = app_label.split(".", 1)[0]
|
||||
nested_path = os.path.join(search_dir, nested_dir_name)
|
||||
if os.path.isdir(nested_path):
|
||||
return True, nested_dir_name, nested_path
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return False, None, search_dir
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "djinitx"
|
||||
version = "0.1.0"
|
||||
version = "0.2.1"
|
||||
description = "A CLI tool to setup Django project"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
click==8.3.0
|
||||
click==8.3.1
|
||||
rich==14.2.0
|
||||
ruff==0.14.2
|
||||
ruff==0.14.6
|
||||
jinja2>=3.1.0
|
||||
@@ -2,7 +2,7 @@ from setuptools import find_packages, setup
|
||||
|
||||
setup(
|
||||
name="djinitx",
|
||||
version="0.1.0",
|
||||
version="0.2.1",
|
||||
description="A CLI tool to setup Django projects",
|
||||
long_description=open("README.md").read(),
|
||||
long_description_content_type="text/markdown",
|
||||
@@ -16,7 +16,6 @@ setup(
|
||||
"click>=8.3.0",
|
||||
"rich>=14.2.0",
|
||||
"jinja2>=3.1.0",
|
||||
"django>=4.2,<6.0",
|
||||
],
|
||||
entry_points={
|
||||
"console_scripts": [
|
||||
|
||||
Reference in new issue
Block a user