38 Commits
Author SHA1 Message Date
sankalp be4d754b34 chore: bump version to 0.2.1 and remove django dependency 2025-11-24 20:17:45 +05:45
sankalp 93c7cd1da2 Merge pull request #12 from S4NKALP/dependabot/pip/main/ruff-0.14.6
chore(deps): bump ruff from 0.14.4 to 0.14.6
2025-11-24 14:19:50 +05:45
sankalp 28d298679e Merge pull request #10 from S4NKALP/dependabot/github_actions/main/actions/checkout-6
chore(deps): bump actions/checkout from 5 to 6
2025-11-24 14:19:35 +05:45
sankalp ad5bd8c699 Merge pull request #11 from S4NKALP/dependabot/pip/main/click-8.3.1
chore(deps): bump click from 8.3.0 to 8.3.1
2025-11-24 14:19:18 +05:45
dependabot[bot] 08645b9d90 chore(deps): bump ruff from 0.14.4 to 0.14.6
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.14.4 to 0.14.6.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.14.4...0.14.6)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.14.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-24 02:34:06 +00:00
dependabot[bot] 3f39e458c7 chore(deps): bump click from 8.3.0 to 8.3.1
Bumps [click](https://github.com/pallets/click) from 8.3.0 to 8.3.1.
- [Release notes](https://github.com/pallets/click/releases)
- [Changelog](https://github.com/pallets/click/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/click/compare/8.3.0...8.3.1)

---
updated-dependencies:
- dependency-name: click
  dependency-version: 8.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-24 02:33:57 +00:00
dependabot[bot] fe9c38e278 chore(deps): bump actions/checkout from 5 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-24 02:29:32 +00:00
sankalp b7a8e20595 chore(asgi/wsgi): set default DJANGO_SETTINGS_MODULE for production environments 2025-11-13 02:33:16 +05:45
sankalp a575863ca0 feat(cors): configure CORS settings for development and production environments 2025-11-13 01:52:56 +05:45
sankalp 3aef01d4b5 chore: update .env.sample with new Discord bot token key 2025-11-09 19:45:51 +05:45
sankalp 6c882864b4 chore: remove django as a direct dependency 2025-11-09 15:34:20 +05:45
sankalp fc5de30cb7 docs: improve README with detailed features, structures, commands, and setup instructions 2025-11-09 15:30:56 +05:45
sankalp 211219247d Merge pull request #9 from S4NKALP/dependabot/pip/main/ruff-0.14.4
chore(deps): bump ruff from 0.14.2 to 0.14.4
2025-11-09 15:26:21 +05:45
sankalp 6fefe8d948 Merge pull request #6 from S4NKALP/dependabot/github_actions/main/actions/setup-python-6
chore(deps): bump actions/setup-python from 5 to 6
2025-11-09 15:25:54 +05:45
sankalp b59f4b3f6e Merge pull request #7 from S4NKALP/dependabot/github_actions/main/actions/checkout-5
chore(deps): bump actions/checkout from 4 to 5
2025-11-09 15:25:41 +05:45
sankalp a8ce63d285 chore: bump version to 0.2.0 2025-11-09 15:23:43 +05:45
dependabot[bot] 82d0af049a chore(deps): bump ruff from 0.14.2 to 0.14.4
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.14.2 to 0.14.4.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.14.2...0.14.4)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.14.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-09 09:35:46 +00:00
sankalp 78cfdb1cc0 Merge pull request #5 from S4NKALP/dev
v0.2.0
2025-11-09 15:19:49 +05:45
sankalp 84db52c906 chore: remove redundant labels from dependabot config 2025-11-09 15:17:33 +05:45
sankalp 30ac34478b refactor(structure): streamline project structure creation and file generation 2025-11-09 15:10:09 +05:45
sankalp cdaeede45a refactor: improve project creation and app generation for better compatibility 2025-11-09 14:26:00 +05:45
sankalp 33e87e7764 feat: add unified project structure option 2025-11-08 23:01:37 +05:45
sankalp 079dfff8a5 chore: remove UIFormatter call to prevent premature output 2025-11-08 21:58:46 +05:45
sankalp e6ce03fa3c docs(ui): update command instructions to reflect just commands 2025-11-08 21:53:20 +05:45
sankalp e34faae3a7 feat: enhance project setup with .env.sample, improved .gitignore, and app structure support 2025-11-08 21:51:18 +05:45
sankalp 21823fcc61 fix(cli): set default package name to 'backend' when project dir is '.' or empty 2025-11-08 21:46:35 +05:45
sankalp eb59e0c487 fix(justfile): escape Jinja2 variables to prevent premature evaluation 2025-11-08 21:43:20 +05:45
sankalp 105cd75595 feat: add predefined project structure option with core and users apps 2025-11-07 10:48:01 +05:45
sankalp 71195af155 feat: improve project creation and input handling for better user experience 2025-11-06 15:07:36 +05:45
dependabot[bot] 5fa2003ae1 chore(deps): bump actions/checkout from 4 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-03 02:36:51 +00:00
dependabot[bot] a9e066e694 chore(deps): bump actions/setup-python from 5 to 6
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-03 02:36:46 +00:00
sankalp 5da43ab3a5 feat: add "generated with djinit" comment to template files 2025-11-03 01:08:21 +05:45
sankalp f45bacf682 fix(ui): improve clarity of user prompts during initialization 2025-11-03 00:53:49 +05:45
sankalp 5a6e51f52f style: remove trailing whitespace from templates 2025-11-03 00:25:46 +05:45
sankalp 022a5a90af refactor: streamline project setup and file generation 2025-11-03 00:20:21 +05:45
sankalp f7c9a0764f style: standardize prompt formatting and input handling 2025-11-02 13:47:12 +05:45
sankalp 462251059c refactor: improve project directory handling and subprocess execution for compatibility 2025-11-02 13:41:30 +05:45
sankalp 4369cdd44f docs: update README with PyPI package name and installation instructions 2025-10-29 13:56:24 +05:45
79 changed files with 2250 additions and 1151 deletions

No files matched your search

-6
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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
View File
@@ -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
+342 -80
View File
@@ -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
![djinit demo](docs/demo.gif)
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
View File
@@ -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
View File
@@ -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
View File
@@ -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
-41
View File
@@ -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
+7 -19
View File
@@ -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"))
+16 -6
View File
@@ -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
+101
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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")
-86
View File
@@ -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
-1
View File
@@ -76,5 +76,4 @@ class TemplateEngine:
return os.path.exists(template_path)
# Global template engine instance
template_engine = TemplateEngine()
+5
View File
@@ -0,0 +1,5 @@
"""generated with djinit"""
from django.contrib import admin
# Register your models here.
+8
View File
@@ -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.
+5
View File
@@ -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
+5
View File
@@ -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
+5
View File
@@ -0,0 +1,5 @@
"""generated with djinit"""
from django.shortcuts import render
# Create your views here.
+7
View File
@@ -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
@@ -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
@@ -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
+8
View File
@@ -0,0 +1,8 @@
"""generated with djinit"""
from django.contrib import admin
from django.urls import path
urlpatterns = [
path('admin/', admin.site.urls),
]
+29
View File
@@ -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()
-60
View File
@@ -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/
@@ -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
View File
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.
+148
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+1 -2
View File
@@ -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": [
Generated
+2 -2
View File
@@ -24,8 +24,8 @@ wheels = [
]
[[package]]
name = "djinit"
version = "0.1.0"
name = "djinitx"
version = "0.2.0"
source = { virtual = "." }
dependencies = [
{ name = "click" },